JSPM

@quicktoolsone/pdf-compress

2.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 559
  • Score
    100M100P100Q115533F
  • License MIT

Simple, clean PDF compression library with excellent DX

Package Exports

  • @quicktoolsone/pdf-compress

Readme

@quicktoolsone/pdf-compress

Battle-tested PDF compression library with multi-strategy approach

npm version License: MIT

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-compress

Dependencies: 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

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:

  1. Renders each page with pdf.js at optimized DPI
  2. Adapts DPI based on file size:
    • 50MB+: 50 DPI (extremely aggressive)
    • 20-50MB: 75 DPI
    • 10-20MB: 100 DPI
    • <10MB: 150 DPI
  3. Compresses to JPEG with quality settings per preset:
    • Lossless: N/A (skips this strategy)
    • Balanced: 70% quality
    • Max: 50% quality
  4. Rebuilds PDF with compressed images
  5. 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 compress
  • options (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.mjs

Option 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.mjs

Browser 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.mjs is in public/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: