Compress PDF files in Node.js — no Ghostscript, no binaries, no
postinstalldownloads.
compress-pdf reduces PDF file sizes by recompressing image streams (JPEG via mozjpeg WebAssembly) and optimizing internal data streams (via Node.js built-in zlib). The entire engine ships inside the npm package — nothing to install separately.
| v0.x | v1.0.0 | |
|---|---|---|
| Engine | Ghostscript binary (spawned subprocess) | pdf-lib + mozjpeg WebAssembly |
| Install size | ~50 MB (GS binary downloaded at postinstall) |
~10 MB (WASM bundled) |
| Works on Alpine / Docker slim | ❌ Often breaks | ✅ Always |
| Works on ARM (M1/M2, Pi) | ❌ Depends on binary | ✅ Always |
| Works in serverless (Lambda, Vercel, Fly) | ❌ Binary size + permissions | ✅ Always |
postinstall download |
✅ Required | ❌ Removed |
| License stack | AGPL (GS binary) | MIT + BSD |
npm install compress-pdfyarn add compress-pdfNo environment variables, no binary downloads, no system dependencies required.
import { compress } from 'compress-pdf';
const result = await compress('path/to/file.pdf');
// result is a Buffer — write it directly
await fs.promises.writeFile('compressed.pdf', result);
// Access compression metadata
console.log(`${result.originalSize} → ${result.compressedSize} bytes`);
console.log(`${((1 - result.compressionRatio) * 100).toFixed(1)}% smaller`);
console.log(`Took ${result.duration}ms`);See examples/basic.ts for a self-contained example.
import express from 'express';
import multer from 'multer';
import { compress, CompressPdfError } from 'compress-pdf';
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
app.post('/compress', upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
try {
const result = await compress(req.file.buffer, { resolution: 'ebook' });
res.set('Content-Type', 'application/pdf');
res.set('Content-Disposition', 'attachment; filename="compressed.pdf"');
res.send(result);
} catch (err) {
if (err instanceof CompressPdfError) {
res.status(422).json({ error: err.message });
} else {
res.status(500).json({ error: 'Unexpected error' });
}
}
});Full example: examples/express-server.ts
// app/api/compress/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { compress, CompressPdfError } from 'compress-pdf';
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get('file') as File;
const buffer = Buffer.from(await file.arrayBuffer());
try {
const result = await compress(buffer, { resolution: 'ebook' });
return new NextResponse(result, {
headers: { 'Content-Type': 'application/pdf' },
});
} catch (err) {
if (err instanceof CompressPdfError) {
return NextResponse.json({ error: err.message }, { status: 422 });
}
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}Full example: examples/nextjs-api-route.ts
import { compress, CompressPdfError } from 'compress-pdf';
// Compress a password-protected PDF (keeps the password)
const result = await compress('protected.pdf', {
pdfPassword: 'mysecret',
});
// Compress and remove the password
const unlocked = await compress('protected.pdf', {
pdfPassword: 'mysecret',
removePasswordAfterCompression: true,
});
// Handle wrong password
try {
await compress('protected.pdf', { pdfPassword: 'wrong' });
} catch (err) {
if (err instanceof CompressPdfError) {
console.error(err.message); // "Wrong password for encrypted PDF"
}
}Full example: examples/protected.ts
Encryption support: RC4-40, RC4-128, AES-128. AES-256 (PDF 2.0) is not supported and throws
CompressPdfError.
| Option | Type | Default | Description |
|---|---|---|---|
resolution |
'screen' | 'ebook' | 'printer' | 'prepress' | 'default' |
'ebook' |
Quality preset. Sets DPI and JPEG quality together. |
imageDpi |
number |
preset value | Target DPI for image downsampling (1–600). Overrides the preset's DPI. |
jpegQuality |
number |
preset value | mozjpeg encoder quality (0–100). Overrides the preset's quality. |
pdfPassword |
string |
— | Password to open an encrypted PDF. |
removePasswordAfterCompression |
boolean |
false |
Save the output without a password. Requires pdfPassword. |
imageDpi and jpegQuality can be combined with resolution to partially override a preset:
// Use ebook preset but push JPEG quality lower
await compress('file.pdf', { resolution: 'ebook', jpegQuality: 40 });
// Use printer DPI but cap JPEG quality
await compress('file.pdf', { resolution: 'printer', jpegQuality: 70 });| Preset | DPI | JPEG quality | Best for |
|---|---|---|---|
screen |
72 | 35 | Web display, email — smallest file |
ebook |
150 | 65 | General purpose (default) |
printer |
300 | 85 | Office printing |
prepress |
300 | 95 | Professional print — minimal compression |
default |
150 | 75 | Balanced, slightly better quality than ebook |
Compare all presets side by side: examples/presets.ts
Results depend heavily on file content:
| Content type | Expected reduction |
|---|---|
| Image-heavy PDF | 40–70% |
| Mixed PDF (text + images) | 35–60% |
| Text-only / forms | 8–25% |
Font subsetting is not supported. PDFs with large embedded fonts (CJK scripts, decorative typefaces) will see lower gains compared to Ghostscript-based tools.
All errors are thrown as CompressPdfError:
import { compress, CompressPdfError } from 'compress-pdf';
try {
const result = await compress('file.pdf');
} catch (err) {
if (err instanceof CompressPdfError) {
console.error(err.message); // human-readable message
console.error(err.cause); // original underlying error, if any
}
}| Condition | Error message |
|---|---|
| File not found | "File not found: <path>" |
| Invalid PDF | "Failed to parse PDF: <reason>" |
| Encrypted, no password | "PDF is encrypted, provide pdfPassword" |
| Wrong password | "Wrong password for encrypted PDF" |
| AES-256 (unsupported) | "PDF uses AES-256 encryption, not supported" |
imageDpi out of range |
"Invalid imageDpi: must be between 1 and 600" |
jpegQuality out of range |
"Invalid jpegQuality: must be between 0 and 100" |
npx compress-pdf --file input.pdf --output compressed.pdf
# Options
-f, --file <path> Input PDF path (required)
-o, --output <path> Output PDF path (required)
-r, --resolution <preset> screen | ebook | printer | prepress | default
--imageDpi <n> Target image DPI, 1–600
--jpegQuality <n> JPEG quality, 0–100
--pdfPassword <pass> Password for encrypted PDFs
--removePasswordAfterCompression
-h, --help# Examples
npx compress-pdf -f report.pdf -o report-compressed.pdf
npx compress-pdf -f report.pdf -o report-compressed.pdf -r screen
npx compress-pdf -f report.pdf -o report-compressed.pdf --imageDpi 72 --jpegQuality 40
npx compress-pdf -f secured.pdf -o output.pdf --pdfPassword secret
npx compress-pdf -f secured.pdf -o output.pdf --pdfPassword secret --removePasswordAfterCompressionNo special configuration needed. Works with any standard Node.js image:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "dist/index.js"]FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup package*.json ./
RUN npm install
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "dist/index.js"]FROM node:20-alpine AS build
WORKDIR /src
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /src/dist ./dist
CMD ["node", "dist/index.js"]FROM public.ecr.aws/lambda/nodejs:20
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ ./dist/
CMD ["dist/handler.handler"]No
COMPRESS_PDF_SKIP_DOWNLOADor extra apt packages needed — the library is fully self-contained.
The library is written in TypeScript and ships with full type definitions:
import { compress, CompressPdfError } from 'compress-pdf';
import type { Options, CompressResult, Resolution } from 'compress-pdf';
async function compressPdf(
input: string | Buffer,
options?: Options
): Promise<Buffer & CompressResult> {
return compress(input, options);
}// v0.x
await compress('file.pdf', {
imageQuality: 150, // ← was DPI despite the name
compatibilityLevel: 1.4, // ← GS-specific
gsModule: '/usr/bin/gs', // ← binary path
});
// v1.0.0
await compress('file.pdf', {
imageDpi: 150, // ← renamed for clarity
// compatibilityLevel removed — pdf-lib outputs PDF 1.7
// gsModule removed — no binary
});| v0.x option | v1.0.0 | Notes |
|---|---|---|
imageQuality |
imageDpi |
Renamed — was always DPI |
jpegQuality |
jpegQuality |
New — JPEG encoder quality 0–100 |
compatibilityLevel |
removed | pdf-lib always outputs PDF 1.7 |
gsModule |
removed | No binary path needed |
- Font subsetting: Not supported. PDFs with large embedded fonts compress less than with Ghostscript.
- PNG/TIFF images: Not quality-reduced — only Flate-recompressed. JPEG is the main compression target.
- AES-256 encryption: Not supported (PDF 2.0). RC4 and AES-128 work fine.
- Browser: Not supported in this version. The library uses Node.js built-ins (
fs,zlib).
| File | Description |
|---|---|
examples/basic.ts |
Compress a file and log metadata |
examples/protected.ts |
Compress with password / remove password |
examples/presets.ts |
Compare all resolution presets |
examples/express-server.ts |
Express.js upload endpoint |
examples/nextjs-api-route.ts |
Next.js App Router API route |
MIT — see LICENSE for details.
The WebAssembly modules bundled in this package use the following licenses:
- pdf-lib — MIT
- @jsquash/jpeg — MIT (mozjpeg: BSD)
- @jsquash/resize — MIT