Skip to content

jesuejunior/quire

Repository files navigation

Quire

A small web app for the things you actually do with PDFs:

  • Compress — reduce file size by picking a quality preset or a target size in MB.
  • PDF → JPG — render every page as a JPEG (single page → single JPG; multi-page → ZIP).
  • JPG → PDF — combine images into a single PDF, in the order you upload them.
  • Merge — concatenate multiple PDFs, no re-encoding of pages.
  • Split — one PDF per page, or split by custom ranges (1-3, 5, 8-10).

Everything runs locally. Files never leave your machine.


Requirements

  • Python 3.14+
  • uv — fast Python package manager
    • macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh
    • macOS: brew install poppler
    • Debian/Ubuntu: sudo apt install poppler-utils

Install

uv sync

Run

uv run uvicorn quire:app --port 8000

Open http://localhost:8000

For development with auto-reload:

uv run uvicorn quire:app --reload --port 8000

Docker

docker build -t quire .
docker run -p 8000:8000 quire

How it works

Operation Approach
Compress pdftoppm -scale-to N → JPEG → img2pdf (no double-encoding)
Compress (target) Walks a quality ladder from highest → lowest; stops at first fit
PDF → JPG pdftoppm -jpeg -r DPI
JPG → PDF img2pdf (JPEGs pass through losslessly; other formats re-encoded)
Merge pypdf (lossless, page-level concatenation)
Split pypdf

Using long-edge pixel count (-scale-to) instead of DPI (-r) keeps output sizes predictable even when the source PDF has unusually large page dimensions (scanned documents sometimes wrap a single image on a 24×31-inch page).

Deploying behind Nginx Proxy Manager

The app includes rate limiting, file validation, security headers, and a concurrency cap. For public-facing deployments, add bot filtering at the reverse proxy layer.

In NPM, open your proxy host and paste the following in the Advanced tab:

# Block abusive bots
if ($http_user_agent ~* (SemrushBot|AhrefsBot|MJ12bot|DotBot|BLEXBot|DataForSeoBot|GPTBot|OAI-SearchBot|ChatGPT-User|CCBot|ClaudeBot|Bytespider|PetalBot|Scrapy|wget|curl|python-requests)) {
    return 444;
}

# Block empty User-Agent
if ($http_user_agent = "") {
    return 444;
}

# Block common scanner paths
location ~* (\.php|\.asp|\.env|wp-admin|wp-login|\.git) {
    return 444;
}

# Cap upload size (matches app limit)
client_max_body_size 100m;

For Nginx-level rate limiting, create /data/nginx/custom/http_top.conf on the NPM host:

limit_req_zone $binary_remote_addr zone=quire:10m rate=30r/m;

Then add to the same Advanced tab:

limit_req zone=quire burst=20 nodelay;
limit_req_status 429;

Notes

  • Upload size is capped at 100 MB per file (MAX_UPLOAD_MB in app.py).
  • Working files live under your OS temp dir (quire_work/) and are cleaned up after each request and swept by a background thread hourly.
  • API endpoints are rate-limited to 10 requests/min per IP.
  • At most 4 processing jobs run concurrently; additional requests wait.
  • The app binds to localhost only by default. If you want to expose it on your network, run with --host 0.0.0.0 — but be aware there is no authentication built in.

About

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors