172 lines
6.8 KiB
Markdown
172 lines
6.8 KiB
Markdown
# EPUB → PDF Converter — User Guide
|
||
|
||
A small, self-hosted web app that converts EPUB3 ebooks into typeset PDFs.
|
||
Everything runs locally (or on your own server); **no book ever leaves your machine**.
|
||
|
||
---
|
||
|
||
## 1. What it does (and doesn't)
|
||
|
||
| Does | Doesn't |
|
||
|---|---|
|
||
| Preserves the EPUB's own CSS, images, headings, tables, code blocks | Re-flow or re-typeset like a print publisher would |
|
||
| Renders CJK (Chinese / Japanese / Korean) correctly via Noto fonts | Handle DRM-protected (e.g. protected) EPUBs |
|
||
| Supports right-to-left books (Arabic, Hebrew) | Convert EPUB → other formats (only PDF out) |
|
||
| Keeps book title/author as PDF metadata | — |
|
||
|
||
**Limits:** one book at a time per job (you can queue several, they render one after
|
||
another in the same server process), max upload size **400 MB**.
|
||
|
||
---
|
||
|
||
## 2. Quick start
|
||
|
||
### Option A — Run directly (no Docker)
|
||
```bash
|
||
# 1. install dependencies (Python 3.11+; WeasyPrint also needs the pango
|
||
# system libraries, e.g. on Debian/Ubuntu: sudo apt install libpango-1.0-0
|
||
# libpangoft2-1.0-0 libharfbuzz0b fontconfig fonts-noto-cjk shared-mime-info)
|
||
pip install -r requirements.txt
|
||
|
||
# 2. start the web server
|
||
python app.py # -> http://localhost:8030
|
||
```
|
||
|
||
### Option B — Docker (recommended for a persistent install)
|
||
```bash
|
||
docker build -t epub2pdf .
|
||
docker run -d --name epub2pdf \
|
||
-p 8030:8030 \
|
||
-v epub2pdf-data:/app/data \
|
||
epub2pdf
|
||
```
|
||
The volume `epub2pdf-data` keeps uploaded EPUBs and generated PDFs across restarts.
|
||
|
||
### Option C — Coolify
|
||
See `COOLIFY.md` in this directory (3 steps: Dockerfile service, port 8030, volume at `/app/data`).
|
||
|
||
---
|
||
|
||
## 3. Using the web UI
|
||
|
||
1. Open `http://<host>:8030` in your browser.
|
||
2. **Drop an `.epub` file** onto the upload box (or click to browse).
|
||
- Only `.epub` files are accepted; anything else is rejected with a message.
|
||
- Files over 400 MB are rejected before upload finishes.
|
||
3. The job starts immediately — a progress bar shows which chapter is being
|
||
rendered (e.g. `Rendering chapter-04.xhtml (4/12)`), then `Merging pages…`.
|
||
4. When it finishes, a **Download PDF** button appears.
|
||
- The PDF is named after the book's title (e.g. `My_Book.pdf`).
|
||
5. Repeat for the next book. Old jobs stay listed so you can re-download
|
||
recent PDFs (the server keeps the 50 most recent jobs and their outputs).
|
||
|
||
### What can go wrong in the UI
|
||
| Symptom | Meaning / fix |
|
||
|---|---|
|
||
| `File too large (max 400 MB)` | Shrink or split the book, or raise `MAX_UPLOAD_MB` in `app.py` |
|
||
| `Not a valid EPUB (bad zip container)` | File is corrupted or renamed — re-export from your ebook store/library |
|
||
| `No readable content found in EPUB spine` | The EPUB has no HTML chapters (images-only books, broken exports) |
|
||
| `DRM protected` style errors | Remove DRM first (Calibre / Calibre-DB tools) — this app can't decrypt |
|
||
| Stuck at `Working…` forever | The server restarted mid-job; refresh the page and re-upload |
|
||
|
||
---
|
||
|
||
## 4. Command-line usage (no server)
|
||
|
||
```bash
|
||
# convert one file and exit
|
||
python app.py --cli book.epub
|
||
|
||
# choose where the PDF lands
|
||
python app.py --cli book.epub --output /path/to/dir
|
||
|
||
# inside a running container
|
||
docker exec epub2pdf python app.py --cli /app/data/uploads/book.epub --output /app/data/output
|
||
```
|
||
Progress lines like `[3/12] chapter-03.xhtml` print as each chapter renders.
|
||
|
||
---
|
||
|
||
## 5. HTTP API (for automation)
|
||
|
||
Three endpoints, all JSON:
|
||
|
||
```bash
|
||
# 1. upload (multipart form, field name: file) -> starts a job
|
||
curl -X POST -F "file=@book.epub" http://localhost:8030/api/convert
|
||
# -> {"job_id": "c524be5a9d85"}
|
||
|
||
# 2. poll status
|
||
curl http://localhost:8030/api/jobs/c524be5a9d85
|
||
# -> {"status": "working", "progress": 42.5, "message": "Rendering ch-05.xhtml (5/12)"}
|
||
# -> {"status": "done", "progress": 100, "filename": "Book_Title-20260828-203943.pdf"}
|
||
# -> {"status": "error", "error": "Not a valid EPUB (bad zip container)"}
|
||
|
||
# 3. download the finished PDF
|
||
curl -OJ http://localhost:8030/download/Book_Title-20260828-203943.pdf
|
||
```
|
||
|
||
Other endpoints:
|
||
- `GET /` — the web UI
|
||
- `GET /health` — `{"status":"ok","max_upload_mb":400}` (use for healthchecks)
|
||
|
||
---
|
||
|
||
## 6. Configuration
|
||
|
||
Everything works with defaults. Optional overrides:
|
||
|
||
| Setting | Default | How to change |
|
||
|---|---|---|
|
||
| Port | `8030` | Env `PORT`, or `--port` (web mode) |
|
||
| Data location | `<project>/data` | Env `DATA_DIR` (Docker: volume at `/app/data`) — holds `uploads/`, `output/`, `work/` |
|
||
| Max upload size | `400 MB` | Constant `MAX_UPLOAD_MB` in `app.py` (edit + restart) |
|
||
| Page geometry | A4, 20×18 mm margins | Edit `BASE_CSS` in `app.py` (e.g. `size: A4` → `size: Letter`) |
|
||
| Base typography | Georgia/serif 10.5 pt, line-height 1.65 | Edit `BASE_CSS` in `app.py` — the EPUB's own CSS still layers on top of it |
|
||
| RTL handling | auto (reads EPUB `page-progression-direction`) | automatic; no setting |
|
||
|
||
### Fonts (affects what renders correctly)
|
||
- The Docker image ships **DejaVu** + **Noto CJK** fonts.
|
||
- When running without Docker, install at least: `fontconfig fonts-dejavu-core fonts-noto-cjk`
|
||
(plus `libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b shared-mime-info` for WeasyPrint itself).
|
||
- If a book uses an exotic font not installed, WeasyPrint falls back to the
|
||
nearest available font — layout stays intact, letterforms may differ.
|
||
|
||
---
|
||
|
||
## 7. Where files live
|
||
|
||
```
|
||
<DATA_DIR>/
|
||
├── uploads/ # your uploaded .epub files (kept after conversion)
|
||
├── output/ # generated PDFs (kept; served by /download/<name>)
|
||
└── work/ # per-job temp dir (extracted EPUB + chunk PDFs; deleted after each job)
|
||
```
|
||
|
||
You can delete anything in `uploads/` and `output/` at any time (the server
|
||
just returns 404 for downloads of deleted files). `work/` is always empty when
|
||
idle.
|
||
|
||
---
|
||
|
||
## 8. Troubleshooting
|
||
|
||
| Problem | Check |
|
||
|---|---|
|
||
| Server won't start: `OSError: cannot load library 'libpango-1.0-0'` | Missing system libraries — see §2 Option A note |
|
||
| CJK text shows as boxes (□□□) | `fonts-noto-cjk` not installed — install, then run `fc-cache -f`, restart server |
|
||
| Strange characters (`—`) instead of em dashes | This build reads EPUB text as UTF-8 explicitly; if you see this, an old server is running — restart it |
|
||
| Conversion is slow | Normal: WeasyPrint typesets chapter-by-chapter in-process. Big/illustrated books can take minutes; gunicorn timeout is 1200 s by default |
|
||
| Port already in use | `--port 9000` or change env `PORT` |
|
||
|
||
---
|
||
|
||
## 9. Security notes (self-hosted)
|
||
|
||
- The app has **no login**. Anyone who can reach the port can upload and convert.
|
||
On a LAN, that's usually fine; on the public internet, put it behind a
|
||
reverse proxy with auth, or firewall the port.
|
||
- Uploads are stored under a random job ID (not your filename), and downloads
|
||
are restricted to the output directory (path traversal is blocked).
|
||
- Nothing is transmitted anywhere: conversion happens entirely in-process.
|