惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Blog — PlanetScale
Blog — PlanetScale
B
Blog
GbyAI
GbyAI
爱范儿
爱范儿
月光博客
月光博客
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
腾讯CDC
MyScale Blog
MyScale Blog
V
Visual Studio Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How I Split PDFs in the Browser with Vue 3 and pdf-lib
sunshey · 2026-06-25 · via DEV Community

sunshey

Splitting a PDF is one of those features that sounds trivial until you try to build it. Users expect range input (1-3, 5, 7-9), a per-page option, multiple file downloads, and zero server involvement.

I built en.sotool.top/split/ to do exactly that. Here's how it works with Vue 3 and pdf-lib.


Why Client-Side?

PDFs often contain sensitive information. Contracts, medical records, financial statements. Even a "simple" splitting tool should not force users to upload files to a server.

Client-side benefits:

  • No upload bandwidth or size limits
  • No server storage or cleanup
  • Instant processing for normal files
  • Works offline after the page loads

The tradeoff is that everything has to run in the browser, which limits the libraries you can use.


The Stack

  • Vue 3 — UI and state
  • pdf-lib — Load, manipulate, and save PDFs
  • File API — Read the uploaded file
  • lucide-vue-next — Icons
npm install pdf-lib


Loading the PDF and Counting Pages

First, read the file into an ArrayBuffer and load it with pdf-lib.

import { PDFDocument } from 'pdf-lib'

const pdfFile = ref<File | null>(null)
const totalPages = ref(0)

async function handleFile(files: File[]) {
  if (files.length === 0) return
  pdfFile.value = files[0]
  const bytes = await files[0].arrayBuffer()
  const pdf = await PDFDocument.load(bytes)
  totalPages.value = pdf.getPageCount()
}

Now we know how many pages exist and can show the split UI.


Two Split Modes

I offer two ways to split: by range and per page.

Mode 1: Page Range Input

Users type something like 1-3, 5, 7-9. I parse it into groups of page indices.

function parseRanges(input: string, max: number): number[][] {
  const groups: number[][] = []
  const parts = input.split(',').map(s => s.trim())

  for (const part of parts) {
    if (part.includes('-')) {
      const [start, end] = part.split('-').map(Number)
      const pages = []
      for (let i = start; i <= end && i <= max; i++) {
        pages.push(i - 1)
      }
      if (pages.length) groups.push(pages)
    } else {
      const n = Number(part)
      if (n >= 1 && n <= max) groups.push([n - 1])
    }
  }

  return groups
}

This handles ranges, single pages, and mixed input. Page numbers are 1-based because that is what users expect. Internal indices are 0-based for pdf-lib.

Mode 2: Per Page

Save every page as its own PDF. This is useful when each page is effectively a separate document.


Splitting with pdf-lib

Once we know which pages go where, the logic is:

  1. Load the original PDF
  2. For each output group, create a new empty PDF
  3. Copy the relevant pages
  4. Save and download
async function splitPdf() {
  if (!pdfFile.value) return

  const bytes = await pdfFile.value.arrayBuffer()
  const pdf = await PDFDocument.load(bytes)
  const pageCount = pdf.getPageCount()

  if (splitMode.value === 'size') {
    for (let i = 0; i < pageCount; i++) {
      const newPdf = await PDFDocument.create()
      const [page] = await newPdf.copyPages(pdf, [i])
      newPdf.addPage(page)
      const blob = new Blob([await newPdf.save()], { type: 'application/pdf' })
      downloadBlob(blob, `page_${i + 1}.pdf`)
    }
  } else {
    const ranges = parseRanges(rangeInput.value, pageCount)
    for (let idx = 0; idx < ranges.length; idx++) {
      const newPdf = await PDFDocument.create()
      const pages = await newPdf.copyPages(pdf, ranges[idx])
      pages.forEach(p => newPdf.addPage(p))
      const blob = new Blob([await newPdf.save()], { type: 'application/pdf' })
      downloadBlob(blob, `split_${idx + 1}.pdf`)
    }
  }
}

copyPages preserves the content of the copied pages without re-rendering them, so quality stays intact.


Downloading the Result

A small helper to trigger a file download from a Blob.

function downloadBlob(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = filename
  a.click()
  URL.revokeObjectURL(url)
}

When splitting by range, this runs once per output group. When splitting per page, it runs once per page.


Lessons Learned

1-based indexing everywhere in the UI. Developers think in 0-based arrays. Users think in page numbers. Keep the internal logic 0-based but expose 1-based numbers everywhere the user sees.

Parse input defensively. Users will type 5 - 7, 5—7, or 5,6,7. Strip whitespace, handle dashes, ignore empty parts, and clamp to valid page numbers.

Allow multiple downloads. Splitting produces more than one file. Make sure your download helper works in a loop and does not block the browser.

Preserve quality with copyPages. Do not re-encode the entire PDF. Copy only the pages you need and let pdf-lib handle the rest.

Watch for browser pop-up blockers. Multiple automatic downloads can trigger the blocker. Some users may need to allow downloads from your domain.


Try It

The tool is live at en.sotool.top/split/.

Free, no signup, nothing uploads to a server.

Full source is on GitHub. The split logic is in src/views/Split.vue.


Want More Advanced PDF Tools?

If you need OCR, form editing, digital signatures, or batch processing, Wondershare PDFelement is a solid desktop option. It keeps everything local.

This post contains affiliate links.


Have you built PDF manipulation tools in the browser? What edge cases did you run into?