Compare commits
4
Commits
e96661038d
...
DEV-TEST
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de39bafb43 | ||
|
|
ba7a772e52 | ||
|
|
4f75178916 | ||
|
|
100c9af72d |
@@ -0,0 +1,192 @@
|
||||
# epub2pdf — Document → PDF Converter
|
||||
|
||||
> A small, self-hosted web app that turns 27 document formats — EPUB3 ebooks, Office
|
||||
> documents, TeX/LaTeX, Org, AsciiDoc, RST, Markdown, HTML, plain text, CSV/TSV, JSON, XML,
|
||||
> SVG, images, and existing PDFs — into PDFs. **No file ever leaves your machine.**
|
||||
|
||||
Built for people who want a private, on-premise conversion pipeline: run it behind a firewall,
|
||||
on your own server, with no data egress and no account required.
|
||||
|
||||
---
|
||||
|
||||
## Contents
|
||||
|
||||
- [What it does](#what-it-does)
|
||||
- [Features](#features)
|
||||
- [How it works](#how-it-works)
|
||||
- [Security](#security)
|
||||
- [Quick start](#quick-start)
|
||||
- [Docker (recommended)](#docker-recommended)
|
||||
- [Local (no Docker)](#local-no-docker)
|
||||
- [Coolify](#coolify)
|
||||
- [Command line](#command-line)
|
||||
- [Live sites](#live-sites)
|
||||
- [Documentation](#documentation)
|
||||
- [Source repository](#source-repository)
|
||||
- [Attribution](#attribution)
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
doc2pdf takes a single uploaded file and produces a single PDF. It is **self-hosted**:
|
||||
you run the container on your own hardware, so nothing you upload is processed anywhere else.
|
||||
|
||||
The web UI accepts a drop-and-go upload; an HTTP API and a CLI let you automate it. Every
|
||||
format is detected from the file's real bytes (magic-byte detection), not its extension.
|
||||
|
||||
For a complete format-by-format breakdown, the web UI tour, troubleshooting tables, and the
|
||||
full settings reference, read the [USER_GUIDE.md](USER_GUIDE.md). It is the primary,
|
||||
detailed documentation — this README is only the front door.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **27 input formats → PDF.** EPUB3, HTML/HTML5, Markdown, Office (`.docx`, `.odt`, `.rtf`),
|
||||
TeX/LaTeX, Org, AsciiDoc, reStructuredText, plain text, CSV/TSV, JSON, XML, SVG, images
|
||||
(PNG/JPEG/WebP/TIFF/BMP), and existing PDF.
|
||||
- **No file ever leaves your machine.** There is no data egress — uploads and generated PDFs
|
||||
stay on the host that runs the app.
|
||||
- **Self-hosted and private.** No account, no login, no telemetry. Deploy behind your firewall.
|
||||
- **CJK support.** Ships with Noto CJK fonts so Chinese, Japanese, and Korean render correctly.
|
||||
- **RTL support.** Right-to-left books render correctly for Arabic and Hebrew.
|
||||
- **Faithful Office rendering.** The Office trio renders with **Rich** mode (default) through
|
||||
LibreOffice — preserving fonts, page size, and colors — or falls back to **Standard** mode
|
||||
through pandoc.
|
||||
- **Fast and concurrency-safe.** A single gunicorn worker with 8 threads provides 4
|
||||
concurrent conversion slots. Uploads cap at 400 MB, output caps at 200 MB.
|
||||
- **Tested.** 27 formats supported with 152/152 tests green.
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
A browser upload becomes a server-side job that flows through a small, well-defined pipeline:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Browser[Browser — upload a file] --> API[/api/convert/]
|
||||
API --> Queue[Job queue: 1 worker · 4 slots]
|
||||
Queue --> Family{File family?}
|
||||
Family -->|EPUB, HTML, Markdown, JSON, XML, SVG, images, text| W[WeasyPrint renderer]
|
||||
Family --> Office[".docx · .odt · .rtf"]
|
||||
Office --> Mode{Render mode?}
|
||||
Mode -->|Rich (default)| LO[LibreOffice headless]
|
||||
Mode -->|Standard| PC[Pandoc]
|
||||
W --> Guards[Security guards]
|
||||
LO --> Guards
|
||||
PC --> Guards
|
||||
Guards --> Finalize[finalize PDF tail]
|
||||
Finalize --> Download[Download PDF]
|
||||
```
|
||||
|
||||
Every path — whether EPUB/HTML/Markdown renders through WeasyPrint, or the Office trio renders
|
||||
through LibreOffice or pandoc — passes through the same security guards before the final PDF is
|
||||
written. See the [security section](#security) for details.
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
This release ships with every previously identified finding fixed. The pipeline applies these
|
||||
guards before writing output:
|
||||
|
||||
- **Decompression-bomb guard.** Uploads stream through a byte-counted extractor that aborts
|
||||
mid-stream if extraction exceeds its budget.
|
||||
- **SSRF / data-egress blocking.** Remote resources are validated before rendering, and remote
|
||||
schemes (http, https, ftp, ws, wss, file) are blocked — local `data:` resources still render.
|
||||
- **Stored-XSS stripping.** `<script>` tags are stripped from HTML, SVG, EPUB, Markdown, CSV, XML,
|
||||
and other render paths before they reach the renderer.
|
||||
- **Output cap.** Output PDFs are capped at 200 MB; uploads are capped at 400 MB.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
### Docker (recommended)
|
||||
|
||||
Build the image, then run it with a persistent data volume:
|
||||
|
||||
```bash
|
||||
docker build -t epub2pdf .
|
||||
docker run -d --name epub2pdf \
|
||||
-p 8030:8030 \
|
||||
-v epub2pdf-data:/app/data \
|
||||
epub2pdf
|
||||
```
|
||||
|
||||
The `epub2pdf-data` volume keeps uploads and generated PDFs across restarts. Open
|
||||
http://localhost:8030 in a browser.
|
||||
|
||||
To iterate locally without committing to Docker, `./start.sh` runs the app in the foreground
|
||||
with an auto-restart loop:
|
||||
|
||||
```bash
|
||||
./start.sh # http://0.0.0.0:8030
|
||||
PORT=9000 ./start.sh # custom port
|
||||
```
|
||||
|
||||
### Local (no Docker)
|
||||
|
||||
```bash
|
||||
# 1. install dependencies (Python 3.11+)
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 2. start the web server
|
||||
python app.py # -> http://localhost:8030
|
||||
```
|
||||
|
||||
WeasyPrint also needs the pango system libraries. On Debian/Ubuntu, install at least
|
||||
`libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b fontconfig fonts-noto-cjk shared-mime-info`
|
||||
plus `pandoc` for the office/TeX/markup formats.
|
||||
|
||||
### Coolify
|
||||
|
||||
Deploy as a Dockerfile-based service: publish port **8030** and mount a volume at **`/app/data`**.
|
||||
For the exact three-step walkthrough, read [COOLIFY.md](COOLIFY.md).
|
||||
|
||||
---
|
||||
|
||||
## Command line
|
||||
|
||||
Convert a file and exit, without the web server:
|
||||
|
||||
```bash
|
||||
python app.py --cli book.epub --output ./out
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Live sites
|
||||
|
||||
| Environment | URL |
|
||||
|---|---|
|
||||
| Production (main) | http://epub2pdf.209.126.10.202.sslip.io |
|
||||
| Testing (DEV-TEST) | http://epub2pdftest.209.126.10.202.sslip.io |
|
||||
|
||||
Both run on Coolify and report `running:healthy`.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[USER_GUIDE.md](USER_GUIDE.md)** — the primary, full documentation: format reference, web UI
|
||||
tour, rendering modes, HTTP API, configuration, fonts, and troubleshooting.
|
||||
- **[COOLIFY.md](COOLIFY.md)** — deployment guide for Coolify (Dockerfile service, port, volume).
|
||||
|
||||
---
|
||||
|
||||
## Source repository
|
||||
|
||||
The source lives at:
|
||||
|
||||
https://gitea.network-ewee.com/sirdave73/epub2pdf.git
|
||||
|
||||
Gitea Actions runs the pytest suite on every push and pull request to `main`.
|
||||
|
||||
---
|
||||
|
||||
## Attribution
|
||||
|
||||
epub2pdf is a project of **Network-Ewee.com**.
|
||||
@@ -49,7 +49,7 @@ for _d in (UPLOAD_DIR, OUTPUT_DIR, WORK_DIR):
|
||||
MAX_UPLOAD_MB = 400
|
||||
|
||||
# Semantic version exposed by /health so the UI can show "vX.Y.Z" in the footer.
|
||||
APP_VERSION = "2.0.0"
|
||||
APP_VERSION = "2.1.3"
|
||||
|
||||
# req 3 (health): supported formats advertised to clients. Additive key — the
|
||||
# existing /health contract asserts only status + max_upload_mb.
|
||||
|
||||
+18
-9
@@ -177,6 +177,8 @@
|
||||
.drop.over { border-color: transparent; transform: translateY(-2px); background: rgba(139, 92, 246, 0.08); }
|
||||
.drop.over::before { opacity: 1; }
|
||||
.drop input { display: none; }
|
||||
.render-control { display: flex; flex-direction: column; align-items: center; gap: 10px; margin: 12px auto 26px; }
|
||||
.render-caption { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted-2); }
|
||||
|
||||
.drop-visual {
|
||||
display: grid;
|
||||
@@ -346,13 +348,11 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section id="drop" class="drop" role="button" tabindex="0" aria-label="Upload a document">
|
||||
<div class="drop-visual" aria-hidden="true">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="m7 8 5-5 5 5"/><path d="M5 21h14"/></svg>
|
||||
<div class="render-control">
|
||||
<div class="render-caption">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<span>Rendering style</span>
|
||||
</div>
|
||||
<h1>Drop a file to set it in type</h1>
|
||||
<p class="drop-note">or <span class="browse">browse your files</span> · up to <span id="maxmb">400</span> MB · <code>.epub .docx .odt .md .html .txt .csv .svg .png .pdf</code></p>
|
||||
<p id="droperr" class="drop-err" role="alert"></p>
|
||||
<div class="render-modes" role="radiogroup" aria-label="Rendering mode">
|
||||
<label class="mode-seg">
|
||||
<input type="radio" name="render_mode" value="rich" checked>
|
||||
@@ -370,6 +370,15 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="drop" class="drop" role="button" tabindex="0" aria-label="Upload a document">
|
||||
<div class="drop-visual" aria-hidden="true">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="m7 8 5-5 5 5"/><path d="M5 21h14"/></svg>
|
||||
</div>
|
||||
<h1>Drop a file to set it in type</h1>
|
||||
<p class="drop-note">or <span class="browse">browse your files</span> · up to <span id="maxmb">400</span> MB · <code>.epub .docx .odt .md .html .txt .csv .svg .png .pdf</code></p>
|
||||
<p id="droperr" class="drop-err" role="alert"></p>
|
||||
<input type="file" id="file" accept=".epub,.html,.htm,.txt,.log,.md,.markdown,.csv,.tsv,.json,.xml,.svg,.png,.jpg,.jpeg,.webp,.tiff,.bmp,.pdf,.docx,.odt,.rtf,.tex,.latex,.org,.adoc,.rst">
|
||||
</section>
|
||||
|
||||
@@ -397,7 +406,7 @@
|
||||
|
||||
<footer>
|
||||
<span>Bindery · EPUB → PDF · WeasyPress engine</span>
|
||||
<span class="version">v<span id="foot">—</span></span>
|
||||
<span class="version"><span id="foot">—</span></span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -431,7 +440,7 @@
|
||||
.catch(function () {
|
||||
healthDot.classList.add('offline');
|
||||
healthState.textContent = 'Offline';
|
||||
foot.textContent = 'v';
|
||||
foot.textContent = '—';
|
||||
});
|
||||
|
||||
function openPicker() { fileInput.click(); }
|
||||
@@ -480,7 +489,7 @@
|
||||
if (!OKEXT.test(f.name)) { flashError('Unsupported file type — accepted: .epub .html .htm .txt .log .md .markdown .csv .tsv .json .xml .svg .png .jpg .jpeg .webp .tiff .bmp .pdf .docx .odt .rtf .tex .latex .org .adoc .rst'); return; }
|
||||
// STEP 3: progressive disclosure — toggle only for the office trio (.docx/.odt/.rtf)
|
||||
const isOffice = /\.(docx|odt|rtf)$/i.test(f.name);
|
||||
const toggle = document.querySelector('.render-modes');
|
||||
const toggle = document.querySelector('.render-control');
|
||||
if (toggle) toggle.style.display = isOffice ? '' : 'none';
|
||||
var form = new FormData();
|
||||
form.append('file', f);
|
||||
|
||||
Reference in New Issue
Block a user