A self-hosted web app to compress PDFs and convert images to PDF.
Runs entirely on your machine — no files are sent to any third-party service.
- Upload any PDF up to 500 MB
- Choose from 5 compression levels:
| Preset | Mode | Image Width | JPEG Quality | Typical Reduction |
|---|---|---|---|---|
| Extreme | Render (72 dpi) | 600 px | 40 | ~95% |
| High | Image replace | 700 px | 50 | ~90% |
| Balanced (default) | Image replace | 900 px | 60 | ~85% |
| Quality | Image replace | 1200 px | 72 | ~70% |
| Custom | Your choice | 200–2500 px | 10–95 | you decide |
Two compression strategies:
- Image replace — recompresses JPEG/PNG XObjects inside the PDF while leaving text, vectors, and annotations untouched. Best for photo-heavy PDFs.
- Render — rasterises every page at target DPI. Works on any PDF type; text becomes pixels.
- Accepts JPG, PNG, WEBP, BMP, TIFF, GIF (up to 300 files)
- Drag to reorder pages before converting
- Configurable JPEG quality and max image width
- Each image fits proportionally inside A4 — never upscaled
- Background processing with live progress bar
- Before / after size comparison with reduction percentage
- Job files auto-deleted after 1 hour
- No external dependencies or cloud services
┌─────────────────────────────────────────────┐
│ 📄 PDF Tools │
│ ┌──────────────────┬──────────────────┐ │
│ │ 🗜️ Compress PDF │ 🖼️ Images → PDF │ │
│ └──────────────────┴──────────────────┘ │
│ │
│ ┌─ Drop your PDF here ──────────────────┐ │
│ │ │ │
│ │ 📄 │ │
│ │ Drop your PDF here │ │
│ │ or browse files │ │
│ └───────────────────────────────────────┘ │
│ │
│ Compression Level │
│ [🔴 Extreme][🟠 High][🟡 Balanced][🟢 Quality][⚙️ Custom] │
│ │
│ [ 🗜️ Compress PDF ] │
└─────────────────────────────────────────────┘
git clone https://github.com/your-username/pdf-compressor.git
cd pdf-compressor
docker compose up --buildOpen http://localhost:8000 in your browser.
Job files are stored in a named Docker volume (
pdf_workdir) and persist across restarts.
Requirements: Python 3.10+
git clone https://github.com/your-username/pdf-compressor.git
cd pdf-compressor
pip install -r requirements.txt
uvicorn main:app --reload
# → http://localhost:8000pdf-compressor/
├── main.py # FastAPI app — routes, job management, upload/download
├── processor.py # PDF compression + images-to-PDF logic (PyMuPDF + Pillow)
├── static/
│ ├── index.html # Single-page UI (HTML + CSS — no framework)
│ └── app.js # Drag & drop, polling, progress, download
├── Dockerfile # Multi-stage build, non-root user, health check
├── docker-compose.yml # Named volume, memory limit, auto-restart
├── requirements.txt
├── .gitignore
└── .dockerignore
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
Serve the UI |
GET |
/api/presets |
List compression presets |
POST |
/api/compress |
Submit a PDF for compression |
POST |
/api/convert |
Submit images for PDF conversion |
GET |
/api/status/{job_id} |
Poll job status and progress |
GET |
/api/download/{job_id} |
Download the result PDF |
DELETE |
/api/job/{job_id} |
Delete job files immediately |
| Field | Type | Default | Description |
|---|---|---|---|
file |
file | required | PDF file (max 500 MB) |
preset |
string | balanced |
extreme, high, balanced, quality, custom |
custom_width |
int | 900 | Image width in px (only when preset=custom) |
custom_quality |
int | 60 | JPEG quality 10–95 (only when preset=custom) |
custom_mode |
string | image_replace |
image_replace or render (only when preset=custom) |
| Field | Type | Default | Description |
|---|---|---|---|
files |
file[] | required | Image files (JPG, PNG, WEBP, BMP, TIFF, GIF) |
quality |
int | 85 | JPEG quality for output images |
max_width |
int | 2000 | Max image width in output PDF |
order |
JSON string | upload order | JSON array of filenames in desired page order |
{
"status": "processing",
"progress": 45,
"total": 180,
"input_size": 518123520,
"output_size": null,
"pages": null,
"error": null
}status is one of: processing, done, error.
| Layer | Technology |
|---|---|
| Backend | FastAPI + Uvicorn |
| PDF processing | PyMuPDF (fitz) ≥ 1.24 |
| Image processing | Pillow ≥ 10.2 |
| Frontend | Vanilla HTML / CSS / JavaScript (no framework) |
| Container | Docker + Docker Compose |
| Variable | Default | Description |
|---|---|---|
PORT |
8000 |
Listening port (set via uvicorn args) |
| Constant | Default | Description |
|---|---|---|
MAX_PDF_SIZE |
500 MB | Maximum PDF upload size |
MAX_IMAGE_SIZE |
500 MB | Maximum total image upload size |
MAX_IMAGE_COUNT |
300 | Maximum images per conversion job |
JOB_TTL |
3600 s | Seconds before job files are auto-deleted |
docker build -t pdf-tools:latest .
docker run -p 8000:8000 -v pdf_workdir:/app/workdir pdf-tools:latest- Persistent named volume for job files
- 2 GB memory limit (configurable for very large PDFs)
- Health check via HTTP
restart: unless-stopped
Tested on a 494 MB photo PDF (180 pages, each a 2252×4000 JPEG):
| Preset | Output size | Reduction |
|---|---|---|
| Extreme | ~9 MB | 98% |
| High | ~11 MB | 97.8% |
| Balanced | ~17 MB | 96.6% |
| Quality | ~27 MB | 94.5% |
# Install deps
pip install -r requirements.txt
# Run with auto-reload
uvicorn main:app --reload --port 8000
# Run processor tests
python3 -c "
from pathlib import Path
from processor import compress_pdf_file, images_to_pdf_file
print('processor.py OK')
"MIT — do whatever you like with it.