DEV Community

sunshey
sunshey

Posted on

How to Split PDF by Text Content in the Browser with Vue 3 and pdf-lib

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:

  1. Text extraction from PDF pages
  2. String matching (exact, case-insensitive, regex)
  3. Split point calculation (which page to start the new file)
  4. 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>
Enter fullscreen mode Exit fullscreen mode

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(' ')
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Summary

Building a browser-based text-based PDF splitter involves:

  1. Extracting text from each page using PDF.js
  2. Searching for the target string (with case sensitivity options)
  3. Determining split boundaries based on the chosen mode
  4. Creating a new PDF for each section using pdf-lib
  5. 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)