Removing blank pages from a PDF sounds trivial — until you realize that "blank" can mean many things. Is a page with a faint gray background blank? What about a page that has only a header or footer?
Here's how I built a browser-based blank page detector using PDF.js rendering, canvas pixel analysis, and pdf-lib for the actual removal.
Why client-side?
Server-side tools require uploading your file first. For documents with sensitive content, that's a risk. A browser-based approach:
- Processes everything locally
- Shows you a preview before deleting
- Works offline after loading
- Respects user privacy by design
The stack
- Vue 3 + Composition API
-
PDF.js (
pdfjs-dist) for page rendering - html2canvas-style pixel analysis
- pdf-lib for PDF manipulation
- Vite for bundling
The core algorithm
<script setup lang="ts">
import { ref } from 'vue'
import * as pdfjs from 'pdfjs-dist'
import { PDFDocument } from 'pdf-lib'
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'
interface BlankPageResult {
pageIndex: number
isBlank: boolean
confidence: number // 0-1, higher means more confident it's blank
}
async function detectBlankPages(
file: File,
sensitivity: 'low' | 'medium' | 'high' = 'medium'
): Promise<BlankPageResult[]> {
const arrayBuffer = await file.arrayBuffer()
const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise
const thresholds = { low: 0.01, medium: 0.05, high: 0.1 } // ratio of non-white pixels
const results: BlankPageResult[] = []
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const viewport = page.getViewport({ scale: 1 })
// Create offscreen canvas to render and analyze
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d')!
await page.render({ canvasContext: ctx, viewport }).promise
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const totalPixels = imageData.data.length / 4
let nonWhitePixels = 0
// Analyze pixel by pixel
for (let p = 0; p < imageData.data.length; p += 4) {
const r = imageData.data[p]
const g = imageData.data[p + 1]
const b = imageData.data[p + 2]
// A pixel is considered "white" if all channels are > 240
if (r < 240 || g < 240 || b < 240) {
nonWhitePixels++
}
}
const whiteRatio = nonWhitePixels / totalPixels
const confidence = Math.min(1, thresholds[sensitivity] * 5)
const isBlank = whiteRatio < thresholds[sensitivity]
results.push({
pageIndex: i - 1, // zero-indexed for pdf-lib
isBlank,
confidence: 1 - whiteRatio // higher = more content
})
}
return results
}
</script>
Removing the detected blank pages
Once we know which pages are blank, use pdf-lib to remove them:
async function removeBlankPages(
arrayBuffer: ArrayBuffer,
blankIndices: number[]
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.load(arrayBuffer)
const pageCount = pdfDoc.getPageCount()
// Sort indices in descending order to avoid shifting problems
const sorted = [...blankIndices].sort((a, b) => b - a)
for (const index of sorted) {
if (index >= 0 && index < pageCount) {
pdfDoc.removePage(index)
}
}
return await pdfDoc.save()
}
Key detail: sorting in descending order. If you remove page 5 first, page 6 becomes page 5. By removing from highest index to lowest, positions stay stable.
Visual feedback
Users need to see which pages will be deleted before committing:
async function generateThumbnails(file: File, count: number): Promise<string[]> {
const arrayBuffer = await file.arrayBuffer()
const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise
const thumbnails: string[] = []
for (let i = 1; i <= Math.min(pdf.numPages, count); i++) {
const page = await pdf.getPage(i)
const viewport = page.getViewport({ scale: 0.3 }) // smaller thumbnail
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d')!
await page.render({ canvasContext: ctx, viewport }).promise
thumbnails.push(canvas.toDataURL('image/jpeg', 0.7))
}
return thumbnails
}
Each thumbnail gets a badge: "blank" (red), "likely blank" (yellow), or "has content" (green). Users can override by clicking any page.
Performance considerations
For large PDFs (100+ pages), pixel-by-pixel analysis gets slow. Two optimizations:
- Downscale before analyzing. Don't use full-resolution pages. Scale down to 200px width. Blank detection doesn't need megapixel precision.
- Process pages in parallel using Web Workers. Each worker handles a chunk of pages.
// Downscale example
const SCALE = 0.15 // 15% of original resolution
const viewport = page.getViewport({ scale: SCALE })
At 15% scale, a 3000px-wide page becomes 450px. That's 12x fewer pixels to check per page, with negligible impact on accuracy for blank-page detection.
UX tips from a live tool
At en.sotool.top/remove-blank-pages, we learned:
- Show confidence scores, not binary decisions. "This page is 92% likely blank" is more honest than "blank" or "not blank."
- Let users override. Auto-detection isn't perfect. A single click to keep a flagged page builds trust.
- Warn about edge cases. Pages with very light text, faint watermarks, or custom backgrounds may be misclassified.
- Handle multi-size PDFs separately. Don't apply a global threshold if some pages are letter-sized and others are A4.
Going further
Blank page removal is straightforward in theory but tricky in practice because "blank" is subjective. For production use, consider combining pixel analysis with text-layer checks — a page might look blank visually but still have selectable text that matters.
Want to see the full source? github.com/sunshey/pdf-tool.
If you need desktop-grade PDF editing — batch blank page removal, OCR, or advanced export formats — check out Wondershare PDFelement.
Top comments (0)