Compare commits

...
4 Commits
Author SHA1 Message Date
sirdave73 de39bafb43 docs: add README.md (front-door onboarding doc) and bump version 2.1.2 -> 2.1.3 2026-09-05 07:21:56 -04:00
sirdave73 ba7a772e52 fix ui tighten spacing between trust line and render-mode capsule and add gap before drop box; bump 2.1.1 -> 2.1.2
Test / Tests (Python 3.12) (pull_request) Canceled after 0s
2026-09-04 22:24:18 -04:00
sirdave73 4f75178916 fix ui remove doubled v in version footer and bump to 2.1.1
Test / Tests (Python 3.12) (push) Canceled after 0s
2026-09-04 21:30:24 -04:00
sirdave73 100c9af72d feat(ui): standalone render-mode toggle above upload box + version 2.1.0
- Move High Quality / Fast segmented toggle out of the clickable upload
  zone into a standalone .render-control so clicking the drop box only uploads
- Update JS selector to .render-control; progressive disclosure keeps the
  toggle hidden for non-office files, visible for .docx/.odt/.rtf
- Version bump 2.0.0 -> 2.1.0 (dev-test branch save rule: bump version on save)
2026-09-04 21:17:23 -04:00
3 changed files with 222 additions and 21 deletions
+192
View File
@@ -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**.
+1 -1
View File
@@ -49,7 +49,7 @@ for _d in (UPLOAD_DIR, OUTPUT_DIR, WORK_DIR):
MAX_UPLOAD_MB = 400 MAX_UPLOAD_MB = 400
# Semantic version exposed by /health so the UI can show "vX.Y.Z" in the footer. # 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 # req 3 (health): supported formats advertised to clients. Additive key — the
# existing /health contract asserts only status + max_upload_mb. # existing /health contract asserts only status + max_upload_mb.
+18 -9
View File
@@ -177,6 +177,8 @@
.drop.over { border-color: transparent; transform: translateY(-2px); background: rgba(139, 92, 246, 0.08); } .drop.over { border-color: transparent; transform: translateY(-2px); background: rgba(139, 92, 246, 0.08); }
.drop.over::before { opacity: 1; } .drop.over::before { opacity: 1; }
.drop input { display: none; } .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 { .drop-visual {
display: grid; display: grid;
@@ -346,13 +348,11 @@
</header> </header>
<main> <main>
<section id="drop" class="drop" role="button" tabindex="0" aria-label="Upload a document"> <div class="render-control">
<div class="drop-visual" aria-hidden="true"> <div class="render-caption">
<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> <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> </div>
<h1>Drop a file to set it in type</h1>
<p class="drop-note">or <span class="browse">browse your files</span> &middot; up to <span id="maxmb">400</span>&nbsp;MB &middot; <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"> <div class="render-modes" role="radiogroup" aria-label="Rendering mode">
<label class="mode-seg"> <label class="mode-seg">
<input type="radio" name="render_mode" value="rich" checked> <input type="radio" name="render_mode" value="rich" checked>
@@ -370,6 +370,15 @@
</span> </span>
</label> </label>
</div> </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> &middot; up to <span id="maxmb">400</span>&nbsp;MB &middot; <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"> <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> </section>
@@ -397,7 +406,7 @@
<footer> <footer>
<span>Bindery &middot; EPUB &rarr; PDF &middot; WeasyPress engine</span> <span>Bindery &middot; EPUB &rarr; PDF &middot; WeasyPress engine</span>
<span class="version">v<span id="foot">&mdash;</span></span> <span class="version"><span id="foot">&mdash;</span></span>
</footer> </footer>
<script> <script>
@@ -431,7 +440,7 @@
.catch(function () { .catch(function () {
healthDot.classList.add('offline'); healthDot.classList.add('offline');
healthState.textContent = 'Offline'; healthState.textContent = 'Offline';
foot.textContent = 'v'; foot.textContent = '&mdash;';
}); });
function openPicker() { fileInput.click(); } function openPicker() { fileInput.click(); }
@@ -480,7 +489,7 @@
if (!OKEXT.test(f.name)) { flashError('Unsupported file type &mdash; 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; } if (!OKEXT.test(f.name)) { flashError('Unsupported file type &mdash; 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) // STEP 3: progressive disclosure — toggle only for the office trio (.docx/.odt/.rtf)
const isOffice = /\.(docx|odt|rtf)$/i.test(f.name); 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'; if (toggle) toggle.style.display = isOffice ? '' : 'none';
var form = new FormData(); var form = new FormData();
form.append('file', f); form.append('file', f);