Splitting a PDF by text content is one of the most powerful but least intuitive PDF operations. Unlike splitting by page count (math), bookmarks (tree traversal), or blank pages (pixel analysis), text-based splitting requires understanding the semantic content of each page.
Here's how to build a browser-based PDF text splitter with Vue 3 and pdf-lib.
The approach
The core idea is simple: scan each page for a target string, and when found, cut the document there. But implementing it correctly requires handling:
- Text extraction from PDF pages
- String matching (exact, case-insensitive, regex)
- Split point calculation (which page to start the new file)
- Multiple matches (how to handle repeated keywords)
The stack
- Vue 3 with Composition API
- PDF.js for text extraction
- pdf-lib for PDF manipulation
- Vite for bundling
The core implementation
<script setup lang="ts">
import { ref } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import { PDFDocument } from 'pdf-lib'
const file = ref<File | null>(null)
const searchText = ref<string>('')
const splitMode = ref<'before' | 'after' | 'exact'>('before')
const caseSensitive = ref(false)
const splitting = ref(false)
const matchPages = ref<number[]>([])
async function extractText(page: pdfjsLib.PageProxy): Promise<string> {
const textContent = await page.getTextContent()
return textContent.items
.map(item => ('str' in item ? item.str : ''))
.join(' ')
.trim()
}
async function findMatches(text: string, pattern: string): Promise<number[]> {
const flags = caseSensitive.value ? 'g' : 'gi'
const regex = new RegExp(pattern, flags)
const matches: number[] = []
let match
while ((match = regex.exec(text)) !== null) {
matches.push(match.index)
}
return matches
}
async function splitByText() {
if (!file.value || !searchText.value) return
splitting.value = true
const arrayBuffer = await file.value.arrayBuffer()
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
const pdfLibDoc = await PDFDocument.load(arrayBuffer)
const totalPages = pdfJsDoc.numPages
const lowerSearch = caseSensitive.value
? searchText.value
: searchText.value.toLowerCase()
// Step 1: Find all matching pages
const matchPageIndices: number[] = []
for (let i = 1; i <= totalPages; i++) {
const page = await pdfJsDoc.getPage(i)
const text = await extractText(page)
const searchTarget = caseSensitive.value ? text : text.toLowerCase()
if (searchTarget.includes(lowerSearch)) {
matchPageIndices.push(i - 1) // 0-based
}
}
matchPages.value = matchPageIndices.map(p => p + 1) // 1-based for display
if (matchPageIndices.length === 0) {
splitting.value = false
return // No matches found
}
// Step 2: Determine split boundaries
const boundaries: number[] = []
for (const matchIdx of matchPageIndices) {
switch (splitMode.value) {
case 'before':
boundaries.push(matchIdx)
break
case 'after':
boundaries.push(matchIdx + 1)
break
case 'exact':
boundaries.push(matchIdx)
break
}
}
// Add start and end
boundaries.unshift(0)
if (boundaries[boundaries.length - 1] !== totalPages) {
boundaries.push(totalPages)
}
// Step 3: Create split files
const results: Record<string, Uint8Array> = {}
for (let i = 0; i < boundaries.length - 1; i++) {
const start = boundaries[i]
const end = boundaries[i + 1]
const newPdf = await PDFDocument.create()
const pageIndices = Array.from(
{ length: end - start },
(_, j) => start + j
)
const [copiedPages] = await newPdf.copyPages(pdfLibDoc, pageIndices)
copiedPages.forEach(page => newPdf.addPage(page))
const filename = `section-${i + 1}.pdf`
results[filename] = await newPdf.save()
}
// Package as ZIP
const zip = await createZip(results)
downloadZip(zip, 'split-by-text.zip')
splitting.value = false
}
</script>
Key implementation details
1. Text extraction with PDF.js
PDF.js provides page.getTextContent() which returns all text items on a page:
const textContent = await page.getTextContent()
const text = textContent.items
.map(item => ('str' in item ? item.str : ''))
.join(' ')
Note: str may not exist for all items (e.g., special characters). The type guard 'str' in item handles this.
2. Case sensitivity
PDF text extraction preserves original case. To support case-insensitive search:
const lowerSearch = caseSensitive.value
? searchText.value
: searchText.value.toLowerCase()
const searchTarget = caseSensitive.value ? text : text.toLowerCase()
3. Split mode logic
The split mode determines where the boundary falls relative to the match:
switch (splitMode) {
case 'before':
// New section starts at the matching page
boundary = matchPageIndex
break
case 'after':
// New section starts after the matching page
boundary = matchPageIndex + 1
break
case 'exact':
// Same as 'before' for page-level splitting
boundary = matchPageIndex
break
}
4. Multiple matches on the same page
If the search text appears multiple times on the same page, we only care about the page boundary, not the position within the page. For intra-page splitting, a more complex approach (rendering and cutting at the text position) would be needed.
5. Edge case: match on last page
If the search text appears on the last page, the "after" mode would create an empty final section. Handle this:
// Don't create empty sections
if (end > start) {
// Create the file
}
Summary
Building a browser-based text-based PDF splitter involves:
- Extracting text from each page using PDF.js
- Searching for the target string (with case sensitivity options)
- Determining split boundaries based on the chosen mode
- Creating a new PDF for each section using pdf-lib
- Packaging all outputs as a ZIP
The result: a merged document split into logical sections based on text content. Try it at en.sotool.top/split-by-text.
Top comments (0)