@quicktoolsone/pdf-compress
v2.0.0
Published
Simple, clean PDF compression library with excellent DX
Maintainers
Readme
@quicktoolsone/pdf-compress
Battle-tested PDF compression library with multi-strategy approach
Part of QuickTools.one - Privacy-focused browser-based tools. All processing happens entirely in your browser.
Features
- 🎯 Multi-Strategy Compression: Automatically chooses the best approach for your PDF
- 🔒 100% Client-Side: Zero network requests, complete privacy
- 📦 Three Presets: Lossless, Balanced, Max compression
- 📊 Real-time Progress: Detailed progress updates with page-by-page tracking
- 💪 Handles Large Files: Memory-safe processing of 50MB+ PDFs
- 🌐 Framework Agnostic: Works with React, Vue, Next.js, vanilla JS
- ✅ Production Ready: Powers compression on QuickTools.one
Installation
npm install @quicktoolsone/pdf-compressDependencies: pdf-lib (PDF manipulation) + pdfjs-dist (page rendering)
Quick Start
import { compress } from '@quicktoolsone/pdf-compress';
// Load your PDF
const file = await fetch('document.pdf').then(r => r.arrayBuffer());
// Compress with progress tracking
const result = await compress(file, {
preset: 'balanced', // 'lossless' | 'balanced' | 'max'
onProgress: (event) => {
console.log(`${event.phase}: ${event.progress}%`);
if (event.message) {
console.log(event.message); // e.g., "Compressing page 5/98..."
}
}
});
// Check results
console.log(`Original: ${(result.stats.originalSize / 1024 / 1024).toFixed(2)} MB`);
console.log(`Compressed: ${(result.stats.compressedSize / 1024 / 1024).toFixed(2)} MB`);
console.log(`Saved: ${result.stats.percentageSaved.toFixed(1)}%`);
// Download
const blob = new Blob([result.pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'compressed.pdf';
a.click();Compression Presets
Lossless
Structural optimization only - no quality loss. Best for text-heavy documents.
import { compressLossless } from '@quicktoolsone/pdf-compress';
const result = await compressLossless(pdfBuffer);Expected savings: 5-15% for most PDFs
Balanced ⭐ Recommended
Smart multi-strategy approach that tries lossless first, then image compression if needed.
import { compressBalanced } from '@quicktoolsone/pdf-compress';
const result = await compressBalanced(pdfBuffer);Expected savings: 30-70% for image-heavy PDFs, 10-30% for text PDFs
Max
Aggressive compression with lower DPI and quality. Maximum file size reduction.
import { compressMax } from '@quicktoolsone/pdf-compress';
const result = await compressMax(pdfBuffer);Expected savings: 60-90% for image-heavy PDFs
How It Works
The library uses a proven multi-strategy approach battle-tested on QuickTools.one:
Strategy 1: Lossless Optimization (Fast)
First attempt - uses pdf-lib for structural compression:
- Compresses internal PDF objects with object streams
- Removes redundant data
- Optimizes encoding
For lossless preset: Returns this result For balanced/max presets: Continues to Strategy 2 for better compression
Strategy 2: Image Compression (Powerful)
For image-heavy PDFs, renders and re-compresses images:
- Renders each page with
pdf.jsat optimized DPI - Adapts DPI based on file size:
- 50MB+: 50 DPI (extremely aggressive)
- 20-50MB: 75 DPI
- 10-20MB: 100 DPI
- <10MB: 150 DPI
- Compresses to JPEG with quality settings per preset:
- Lossless: N/A (skips this strategy)
- Balanced: 70% quality
- Max: 50% quality
- Rebuilds PDF with compressed images
- Memory-safe: Cleanup between pages, extra delays for large files
Strategy 3: Choose Best Result
Compares lossless vs image compression vs original and returns the smallest.
API Reference
compress(pdfBuffer, options)
Main compression function with full control.
interface CompressionOptions {
preset: 'lossless' | 'balanced' | 'max';
onProgress?: (event: ProgressEvent) => void;
preserveMetadata?: boolean;
// ... other options (see types)
}
interface CompressionResult {
pdf: ArrayBuffer;
stats: {
originalSize: number;
compressedSize: number;
ratio: number;
bytesSaved: number;
percentageSaved: number;
presetUsed: string;
processingTime: number;
chunksProcessed: number;
};
}Parameters:
pdfBuffer(ArrayBuffer): PDF file to compressoptions(CompressionOptions): Compression settings
Returns: Promise
Convenience Functions
// Lossless compression
compressLossless(pdfBuffer, options?)
// Balanced compression (recommended)
compressBalanced(pdfBuffer, options?)
// Maximum compression
compressMax(pdfBuffer, options?)Progress Events
The onProgress callback receives detailed progress updates:
interface ProgressEvent {
phase: 'chunking' | 'compressing' | 'merging' | 'error-recovery';
progress: number; // 0-100
message?: string; // e.g., "Compressing page 5/98..."
currentChunk?: number;
totalChunks?: number;
estimatedTimeRemaining?: number;
}Framework Examples
React
import { compress } from '@quicktoolsone/pdf-compress';
import { useState } from 'react';
function PDFCompressor() {
const [progress, setProgress] = useState(0);
const [message, setMessage] = useState('');
const [result, setResult] = useState(null);
const handleCompress = async (file: File) => {
const buffer = await file.arrayBuffer();
const compressed = await compress(buffer, {
preset: 'balanced',
onProgress: (event) => {
setProgress(event.progress);
setMessage(event.message || '');
}
});
setResult(compressed);
// Download
const blob = new Blob([compressed.pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'compressed.pdf';
a.click();
};
return (
<div>
<input
type="file"
accept="application/pdf"
onChange={(e) => e.target.files?.[0] && handleCompress(e.target.files[0])}
/>
{progress > 0 && (
<>
<progress value={progress} max={100} />
<p>{message}</p>
</>
)}
{result && (
<p>Saved {result.stats.percentageSaved.toFixed(1)}%
({(result.stats.bytesSaved / 1024 / 1024).toFixed(2)} MB)</p>
)}
</div>
);
}Next.js (App Router)
'use client'
import { compress } from '@quicktoolsone/pdf-compress';
import { useState } from 'react';
export default function CompressPage() {
const [status, setStatus] = useState('');
async function handleCompress(file: File) {
const buffer = await file.arrayBuffer();
const result = await compress(buffer, {
preset: 'balanced',
onProgress: (event) => {
setStatus(`${event.progress}%: ${event.message || ''}`);
}
});
// Download
const blob = new Blob([result.pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
}
return (
<div>
<input
type="file"
accept="application/pdf"
onChange={(e) => e.target.files?.[0] && handleCompress(e.target.files[0])}
/>
<p>{status}</p>
</div>
);
}Vanilla JavaScript
<!DOCTYPE html>
<html>
<body>
<input type="file" id="pdf" accept="application/pdf">
<progress id="progress" value="0" max="100"></progress>
<div id="status"></div>
<div id="result"></div>
<script type="module">
import { compress } from '@quicktoolsone/pdf-compress';
document.getElementById('pdf').addEventListener('change', async (e) => {
const file = e.target.files[0];
const buffer = await file.arrayBuffer();
const result = await compress(buffer, {
preset: 'balanced',
onProgress: (event) => {
document.getElementById('progress').value = event.progress;
document.getElementById('status').textContent = event.message || '';
}
});
document.getElementById('result').textContent =
`Saved ${result.stats.percentageSaved.toFixed(1)}%`;
// Download
const blob = new Blob([result.pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'compressed.pdf';
a.click();
});
</script>
</body>
</html>Setup Requirements
PDF.js Worker File
For image compression to work, you need the pdf.js worker file accessible:
Option 1: Copy to public folder (recommended for apps)
# After npm install
cp node_modules/pdfjs-dist/build/pdf.worker.mjs public/pdf.js/pdf.worker.min.mjsOption 2: Use CDN (automatic fallback) The library automatically falls back to CDN if local worker is not found.
Vite Configuration
If using Vite, add to vite.config.ts:
export default defineConfig({
publicDir: 'public', // Ensure public folder is served
});Next.js Configuration
Copy worker to public folder:
mkdir -p public/pdf.js
cp node_modules/pdfjs-dist/build/pdf.worker.mjs public/pdf.js/pdf.worker.min.mjsBrowser Support
- Chrome/Edge 90+
- Firefox 89+
- Safari 15+
- Mobile: iOS 15+, Android Chrome 90+
Requirements:
- Browser environment (uses Canvas API)
- JavaScript enabled
- Modern ES2020+ support
Expected Compression Results
Results vary based on PDF content:
| PDF Type | Lossless | Balanced | Max | |----------|----------|----------|-----| | Text-heavy PDFs | 10-30% | 15-40% | 20-50% | | Mixed content | 10-25% | 30-60% | 50-80% | | Image-heavy PDFs | 5-15% | 40-70% | 60-90% | | Scanned documents | 5-10% | 50-80% | 70-95% | | Already optimized | 2-10% | 5-20% | 10-30% |
Real example: 41MB scanned workbook (98 pages) → 2.6MB with balanced preset (94% reduction)
Performance Considerations
Processing Time
- Lossless: Very fast (~1-2 seconds for most PDFs)
- Balanced: Moderate (depends on page count and size)
- Small (<10MB): 5-15 seconds
- Medium (10-20MB): 15-45 seconds
- Large (20-50MB): 45-90 seconds
- Very large (50MB+): 2-5 minutes
- Max: Similar to balanced (aggressive DPI helps with large files)
Memory Usage
The library is designed to handle large files safely:
- Garbage collection between pages
- Extra delays for very large files (50MB+)
- Adaptive DPI based on file size
- Canvas cleanup after each page
Typical memory usage: 100-300MB peak during processing
Error Handling
import { compress, CompressionError } from '@quicktoolsone/pdf-compress';
try {
const result = await compress(pdfBuffer, { preset: 'balanced' });
} catch (error) {
if (error instanceof CompressionError) {
console.error('Compression failed:', error.message);
console.log('Attempted preset:', error.attemptedPreset);
console.log('Original size:', error.originalSize);
console.log('Phase:', error.phase);
} else {
console.error('Unexpected error:', error);
}
}Common Issues
"Failed to load PDF.js worker"
- Ensure
pdf.worker.min.mjsis inpublic/pdf.js/folder - Or let it fall back to CDN (automatic)
"Out of memory" errors
- Try a lighter preset (balanced instead of max)
- Process smaller PDFs
- Close other browser tabs to free memory
Compression doesn't reduce size much
- PDF may already be optimized
- Text-heavy PDFs compress less than image-heavy ones
- Try different presets to see which works best
TypeScript Support
Full TypeScript support with detailed type definitions:
import {
compress,
type CompressionOptions,
type CompressionResult,
type ProgressEvent,
CompressionError
} from '@quicktoolsone/pdf-compress';License
MIT License - see LICENSE file
About QuickTools
This library powers the PDF compression tool at QuickTools.one, a collection of privacy-first browser tools for document manipulation. The compression algorithm has been tested on thousands of PDFs in production.
Support & Contributing
Credits
Built with:
- pdf-lib - PDF manipulation
- pdf.js - Page rendering
- Tested and refined on QuickTools.one
