How to Split PDF by File Size in the Browser with Vue 3 and pdf-lib

dev.to

Splitting a PDF by file size is one of the most practical but technically tricky operations. Unlike splitting by page count (simple math) or bookmarks (tree traversal), size-based splitting requires estimating and controlling the output size of each chunk — and PDFs don't have a simple "size per page" property.

Here's how to build a browser-based PDF splitter that respects file size constraints.

The challenge

PDFs are notoriously unpredictable in terms of size. Two PDFs with the same number of pages can differ by 10x in file size depending on:

  • Image resolution and compression
  • Font embedding
  • Color space (RGB vs. CMYK)
  • Content complexity (vector graphics vs. scanned images)

This means you can't calculate split points with simple arithmetic. You need to estimate, test, and adjust.

The stack

  • Vue 3 with Composition API
  • pdf-lib for PDF manipulation
  • Vite for bundling

The core implementation

The approach is greedy accumulation with size estimation:

<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument } from 'pdf-lib'

const file = ref<File | null>(null)
const targetSizeMB = ref<number>(10)
const compression = ref<'none' | 'low' | 'high'>('low')
const splitting = ref(false)
const progress = ref(0)
const progressTotal = ref(0)
const results = ref<Record<string, Uint8Array>>({})

async function splitBySize() {
  if (!file.value) return
  splitting.value = true

  const arrayBuffer = await file.value.arrayBuffer()
  const pdf = await PDFDocument.load(arrayBuffer)
  const totalPages = pdf.getPageCount()
  const targetBytes = targetSizeMB.value * 1024 * 1024

  const outputFiles: Array<{ name: string; data: Uint8Array }> = []
  let currentPdf = await PDFDocument.create()
  let currentSize = 0
  let pageNum = 0

  for (let i = 0; i < totalPages; i++) {
    progressTotal.value = totalPages
    progress.value = i + 1

    // Try adding this page
    try {
      const [copiedPage] = await currentPdf.copyPages(pdf, [i])
      currentPdf.addPage(copiedPage)

      // Estimate size by saving
      const estimated = await currentPdf.save({ 
        compress: compression.value !== 'none',
        updateMetadata: false,
      })
      currentSize = estimated.byteLength

      // If we'd exceed target and this isn't the last page, close current file
      if (currentSize >= targetBytes && i < totalPages - 1) {
        outputFiles.push({
          name: `part-${pageNum + 1}.pdf`,
          data: await currentPdf.save({ compress: true }),
        })
        pageNum++
        currentPdf = await PDFDocument.create()
        currentSize = 0
        // Retry adding this page to the new file
        i-- // Re-process this page in the new file
        continue
      }
    } catch {
      // Page couldn't be copied — skip it
      console.warn(`Page ${i + 1} skipped due to error`)
    }
  }

  // Don't forget the last file
  if (currentSize > 0) {
    outputFiles.push({
      name: `part-${pageNum + 1}.pdf`,
      data: await currentPdf.save({ compress: true }),
    })
  }

  results.value = Object.fromEntries(
    outputFiles.map(f => [f.name, f.data])
  )
  splitting.value = false
}
</script>
Enter fullscreen mode Exit fullscreen mode

Key implementation details

1. Why incremental save for estimation?

PDF.js and pdf-lib don't provide a reliable "what will this page contribute to file size?" API. The most accurate estimation is to actually save the PDF and check the byte length. This is done incrementally:

const estimated = await currentPdf.save()
currentSize = estimated.byteLength
Enter fullscreen mode Exit fullscreen mode

This is slow for large documents (each save is O(pages²) in the worst case), but it's accurate. For better performance with very large PDFs, you could use a sampling approach: save every 5th page to estimate, then adjust.

2. The retry logic

When a page would push the current file over the size limit, we close the current file and re-process the same page in a new file:

if (currentSize >= targetBytes && i < totalPages - 1) {
  outputFiles.push(...)
  currentPdf = await PDFDocument.create()
  currentSize = 0
  i-- // Re-process this page
  continue
}
Enter fullscreen mode Exit fullscreen mode

This ensures no page is lost — even if it doesn't fit in the target size on its own, it goes into its own file.

3. Compression settings

pdf-lib supports compression when saving:

await pdf.save({ compress: true })   // Compress streams (JPEG for images)
await pdf.save({ compress: false })  // Keep original compression
Enter fullscreen mode Exit fullscreen mode

Higher compression reduces file size but may reduce image quality. Let the user choose:

  • None: No additional compression (fastest, largest files)
  • Low: Mild compression (good balance)
  • High: Aggressive compression (smallest files, lower quality)

4. Progress indication

Since size estimation requires incremental saves, large PDFs can take time. Show progress:

progress.value = i + 1  // Current page being processed
progressTotal.value = totalPages
Enter fullscreen mode Exit fullscreen mode

5. Edge case: single page exceeds target

If even a single page exceeds the target size (e.g., a full-page high-resolution scanned image), the algorithm will still create a file for it. The user should be warned that some files may exceed the target size.


Summary

Building a browser-based "split by file size" tool involves:

  1. Loading the PDF and setting a target size
  2. Greedily accumulating pages into output files
  3. Estimating file size via incremental saves
  4. Closing files and starting new ones when targets are exceeded
  5. Packaging all outputs as a ZIP

The result: a large PDF split into chunks that respect your size constraints. Try it at en.sotool.top/split-by-size.

Source: dev.to

arrow_back Back to News