How Split PDF Reads "1-3, 5, 8-" and Turns It Into Files

A visa portal wants only the page with your signature. An accountant wants the March statement, not the whole year. A school portal rejects anything over a few megabytes, so the 40-page scan has to go in parts. Every one of those is a split, and every one needs a different kind of split.

When I built ToolOnTap’s Split PDF, most of the work wasn’t in the PDF library. It was in reading what people type into a small text box, like 1-3, 5, 8-, and turning it into the exact files they expect.

Four ways to split, one plan

The tool has four modes:

Mode What you get Example on a 10-page file
Every page One PDF per page 10 files
Every few pages Parts of N pages, the last one may be shorter every 3 pages gives 1-3, 4-6, 7-9, 10
Custom ranges One PDF per range you type 1-3, 4-8, 9- gives 3 files
Extract pages The pages you choose, in one PDF 5, 1-2 gives one file with pages 5, 1, 2

All four go through one function, splitParts, which returns a plan before any PDF is touched: a list of parts, each with a file name, a label like “Pages 1-3” and the 0-based page indexes it holds. The page shows that plan under the text box (“3 files: 1-3 · 4-8 · 9-10”) and numbers each page preview with the file it will go into. So a typo shows up as a wrong number on a thumbnail, before you’ve waited for anything.

Keeping the plan as plain data also made it easy to test. The tests check the plan for each mode without loading a real PDF.

Reading what people type

People write page ranges in many ways: 1-3, 1 - 3, 1–3 with an en dash pasted from Word, -3 for “up to 3”, 8- for “8 to the end”. They separate them with commas, sometimes semicolons, and leave a stray comma at the end. The parser accepts all of those. It splits the input on commas and semicolons, trims each piece, drops empty ones, and checks each piece against two patterns:

// "4-7", "4 – 7", "-3" (up to 3), "8-" (8 to the end) or a single page
const RANGE = /^(\d+)?\s*[-–—]\s*(\d+)?$/
const PAGE = /^\d+$/

Both numbers in RANGE are optional, so the same pattern covers open ends. A missing start means page 1, and a missing end means the last page:

else if (range && (range[1] || range[2])) {
  from = range[1] ? Number(range[1]) : 1
  to = range[2] ? Number(range[2]) : pageCount
}

The range[1] || range[2] check matters. Without it, a lone - would match and quietly mean “every page”. With it, - is an error.

On a 10-page PDF, here is what comes out:

Typed Result
1-3, 5, 8- pages 1–3, page 5, pages 8–10
-3 pages 1–3
2 – 4 ;7 pages 2–4, page 7
1-3,, pages 1–3

Errors that say what to fix

A parser for people should never answer with “invalid input”. Each failure has its own message, and the message names the piece that’s wrong:

Typed (10-page PDF) Message
empty Enter page numbers, like 1-3, 5.
abc or 1.5 “abc” isn’t a page or range. Use numbers like 2 or 4-7.
0 Pages start at 1.
12 This PDF has only 10 pages, so “12” is out of range.
3-1 “3-1” goes backwards. Write it as 1-3.

I chose to reject 3-1 rather than flip it silently. Someone who typed it may have meant something else, and the fix takes a second. The message also waits until you’ve typed something: an empty box in “Custom ranges” shows a hint, not an error in red.

Page numbers here are always the PDF’s own order, counted from 1. That isn’t always the number printed on the page. A report with a cover and a contents page may print “1” on its third page. The tool’s tips say so, because it’s the most common reason a split looks wrong.

Order, overlaps and repeats

Two decisions here are easy to get wrong, and the two range modes handle them differently on purpose.

In Custom ranges, each range becomes its own file, in the order you typed, and overlaps are allowed. 5-6, 1-2, 2-3 makes three files, and page 2 is in two of them. That’s useful when two people each need a section and both sections include the same summary page.

In Extract pages, everything goes into one file. There, a repeated page is almost always a mistake, so each page is kept once, at its first position:

// 0-based indexes of all ranges, each page once, in the order they were typed ("5, 1-2" -> [4, 0, 1])
export function uniqueIndexes(ranges: PageRange[]): number[] {
  return [...new Set(ranges.flatMap(rangeIndexes))]
}

A JavaScript Set keeps insertion order, so this one line removes repeats and keeps the typed order. 5, 1-2, 2 gives pages 5, 1, 2. That means Extract pages can also reorder: type 5, 1-2 and page 5 comes first.

In Extract mode you can also tap page previews to add or remove them. The text box stays the single source of truth. A tap rebuilds the text with formatPages, which collapses runs back into ranges, so tapping pages 5, 1, 2 and 3 writes 5, 1-3 into the box. What you see typed is always exactly what you’ll get.

File names you can sort

Each part gets a name from the original file and its pages:

  • report.pdf, pages 1 to 3, becomes report-pages-1-3.pdf
  • page 5 becomes report-page-5.pdf
  • Extract pages makes report-extracted.pdf

The .pdf extension is matched in any case, so report.PDF works the same way. A file named just .pdf falls back to document. When you download everything, the ZIP is called report-split.zip. If two parts would get the same name, for example because you typed the same range twice, the ZIP adds (1) to the second one so neither file overwrites the other.

The ZIP library (JSZip) is only downloaded when you press “Download all (ZIP)”. The files go into it with STORE, which means no extra compression. Most PDFs already compress their own content, so a second round would mostly cost time.

Copying pages, not redrawing them

The split itself uses pdf-lib. For each part it creates a new, empty PDF, copies the pages into it and saves it:

const out = await PDFDocument.create()
out.setProducer('ToolOnTap')
out.setCreator('ToolOnTap – toolontap.com')
for (const page of await out.copyPages(doc, part.indexes)) out.addPage(page)
blobs.push(new Blob([(await out.save()) as BlobPart], { type: 'application/pdf' }))
onProgress?.(i + 1)

copyPages copies each page’s content and everything it uses: fonts, images, the page size. Nothing is turned into an image, so text stays sharp and selectable, and links to websites keep working.

It also explains a question people ask: why is each part almost as big as the original? A PDF stores a font or a logo once and shares it across pages. A new file needs its own copy of everything its pages use, so ten single-page files can add up to more than the one file they came from.

The parts are built one at a time, so the button can say “Creating file 3 of 10…” on a long document. The original file isn’t changed; the tool only reads it. Password-protected PDFs are refused with a message when you open them, because pdf-lib can’t copy pages out of an encrypted file.

Try it

Split PDF takes files up to 100 MB. If you need to join the parts back together, or combine pages from several PDFs, the other tools on the PDF tools page do that, all in your browser.

Tools in this article

  • Split a PDF

    Split a PDF into single pages or custom ranges, or pull out just the pages you need, right in your browser. No upload, no sign-up.

More articles