Local CLI to convert EPUB files into machine-readable PDFs.
Project description
epub2pdf
Local CLI for turning EPUB files into searchable PDFs, plus optional AI-readable extraction from existing PDFs.
The project is intentionally CLI-first. It does not run a long-lived server for the default workflows, and Codex/OpenCode integrations are thin wrappers around the same installed epub2pdf command.
What It Does
- Converts
.epubfiles into selectable/searchable PDFs. - Writes optional EPUB sidecars as structured JSON, normalized HTML, Markdown, and AI-friendly JSONL (one chapter per line).
- Inspects EPUB metadata, manifest, spine, TOC, and chapter order without rendering.
- Extracts Markdown, JSON, text, or HTML from existing PDFs through native backends (no Java required by default).
- Installs global Codex and OpenCode skills that call this CLI instead of duplicating conversion logic.
Requirements
- Python 3.10+
- WeasyPrint system libraries for the default renderer (Pango, Cairo, GDK-PixBuf)
- Playwright Chromium only when using
--engine playwright - Java 11+ only when using
--engine opendataloaderforpdf-extract - macOS/Linux shell environment
Check runtime dependencies:
python3 --version
Install
From PyPI (recommended):
python3 -m pip install epub2pdf-cli
From source:
python3 -m pip install -e .
Install the optional Playwright backend for Chromium-based rendering:
python3 -m pip install epub2pdf-cli[playwright]
playwright install chromium
Install enhanced PDF extraction backends:
# Best structured extraction (tables, reading order, OCR)
python3 -m pip install epub2pdf-cli[docling]
# Table specialist
python3 -m pip install epub2pdf-cli[pdfplumber]
# Legacy Java-based extractor
python3 -m pip install epub2pdf-cli[legacy-pdf]
Install the MCP server for Claude Desktop:
python3 -m pip install epub2pdf-cli[mcp]
Or use the Docker image (no local Python dependencies):
docker run --rm -v "$PWD:/workspace" ghcr.io/min9lin9/epub2pdf:0.3.0 \
convert book.epub --no-validate
Use ghcr.io/min9lin9/epub2pdf:main for the latest development build.
Install Codex/OpenCode skill wrappers globally:
python3 scripts/install_agent_skills.py
The installer copies templates into:
~/.codex/skills/epub2pdf/
~/.config/opencode/skills/epub2pdf/
Quick Start
Convert an EPUB to PDF:
epub2pdf convert book.epub
Convert with stable output paths and sidecars:
epub2pdf convert book.epub \
--output book.pdf \
--sidecar-json book.json \
--sidecar-html book.html \
--sidecar-markdown book.md \
--sidecar-jsonl book.jsonl
Skip PDF validation to speed up batch pipelines:
epub2pdf convert book.epub --no-validate
Convert multiple EPUBs in parallel:
epub2pdf batch *.epub \
--output-dir out/ \
--workers 4 \
--sidecar-json \
--sidecar-jsonl \
--force
Inspect an EPUB before rendering:
epub2pdf inspect book.epub --json book.inspect.json
Extract Markdown and JSON from an existing PDF:
epub2pdf pdf-extract book.pdf \
--output-dir book_extracted \
--format markdown,json
Use a specific extraction backend:
epub2pdf pdf-extract book.pdf \
--engine docling \
--output-dir book_extracted \
--format markdown,json
CLI Reference
convert
epub2pdf convert INPUT.epub [options]
Common options:
-o, --output PATH: output PDF path. Defaults to the input basename with.pdf.--engine playwright|weasyprint: rendering backend. Default:weasyprint.--sidecar-json PATH: write structured conversion metadata.--sidecar-html PATH: write the normalized merged HTML used for rendering.--sidecar-markdown PATH: write a Markdown version of the EPUB.--sidecar-jsonl PATH: write an AI-friendly JSONL sidecar (one chapter per line).--page-size A4|Letter: output page size. Default:A4.--margin-mm N: page margin in millimeters. Default:12.--cover first|none: include or skip the detected cover image. Default:first.--no-validate: skip PDF validation after rendering.--force: overwrite an existing output PDF.
On success, stdout prints only the PDF path.
batch
epub2pdf batch INPUT1.epub INPUT2.epub ... [options]
Common options:
-o, --output-dir DIR: required output directory for PDFs and sidecars.-j, --workers N: number of parallel worker processes. Default:1.--engine playwright|weasyprint: rendering backend. Default:weasyprint.--sidecar-json: write a JSON report next to each PDF.--sidecar-html: write normalized merged HTML next to each PDF.--sidecar-markdown: write Markdown next to each PDF.--sidecar-jsonl: write an AI-friendly JSONL sidecar next to each PDF.--no-validate: skip PDF validation after rendering.--force: overwrite existing outputs.
On success, stdout prints one output PDF path per line.
inspect
epub2pdf inspect INPUT.epub [--json PATH]
Use this when an agent or script needs EPUB structure before rendering. Without --json, the report is written to stdout.
pdf-extract
epub2pdf pdf-extract INPUT.pdf [options]
Common options:
-o, --output-dir DIR: output directory. Defaults to<pdf-stem>_extracted.--engine pypdfium2|docling|pdfplumber|opendataloader: extraction backend. Default:pypdfium2.--format LIST: comma-separated formats. Default:markdown,json.--pages SPEC: page selection, for example1,3,5-7.--password: password for encrypted PDF files.--use-struct-tree: use tagged PDF structure when available.--sanitize: redact common sensitive data patterns.--keep-line-breaks: preserve original line breaks.--include-header-footer: include page headers and footers.--detect-strikethrough: detect strikethrough text in Markdown/HTML.--table-method default|cluster: table detection mode.--reading-order off|xycut: reading order algorithm. Default:xycut.--image-output off|embedded|external: extracted image handling. Default:external.--image-dir: directory for extracted images.--threads: worker thread count for native extraction.--sidecar-json PATH: write structured extraction report JSON to this path.--force: overwrite existing extraction outputs.
On success, stdout prints one created output path per line.
Extraction Backends
| Engine | Speed | Quality | Best for | Dependencies |
|---|---|---|---|---|
pypdfium2 |
Fastest | High text fidelity | Digital text PDFs | Bundled with base install |
docling |
Moderate | Best structure/tables | Complex layouts, tables, OCR | pip install -e '.[docling]' |
pdfplumber |
Slower | Excellent tables | Table-heavy financial docs | pip install -e '.[pdfplumber]' |
opendataloader |
Moderate | Highest overall accuracy | Legacy high-quality extraction | Java 11+ + pip install -e '.[legacy-pdf]' |
AI-Readable Outputs
convert --sidecar-json writes a stable report with:
source: input path, hash, conversion timestampmetadata: title, language, creators, identifiers, publisher, datesmanifest: EPUB manifest items and media typesspine: ordered reading sequencetoc: table of contents entrieschapters: chapter ids, hrefs, titles, text statistics, anchorsassets: rewritten images, CSS assets, and embedded resourceswarnings: unsupported media, missing assets, image-heavy chaptersoutput: PDF path, backend, page size, validation summary
convert --sidecar-html writes the normalized merged HTML that was rendered into the PDF.
convert --sidecar-markdown writes a Markdown version of the EPUB suitable for RAG ingestion.
convert --sidecar-jsonl writes one JSON object per chapter, optimized for LLM/RAG pipelines:
{"record_type": "chapter", "book_title": "Book Title", "chapter_index": 1, "title": "Chapter 1", "text": "...", "word_count": 1200}
{"record_type": "chapter", "book_title": "Book Title", "chapter_index": 2, "title": "Chapter 2", "text": "...", "word_count": 950}
pdf-extract writes Markdown/JSON/text/HTML files from an existing PDF. This is the path to use when the source is already a PDF rather than an EPUB.
Programmatic API
Use epub2pdf_cli.api.Epub2Pdf to convert from Python. When the Playwright
engine is selected, use the client as a context manager to keep one browser
process alive across multiple conversions.
from epub2pdf_cli.api import Epub2Pdf
# WeasyPrint (no context manager required)
client = Epub2Pdf(engine="weasyprint")
client.convert("book.epub", "book.pdf")
# Playwright with browser pooling
with Epub2Pdf(engine="playwright") as client:
client.convert("a.epub", "a.pdf")
client.convert("b.epub", "b.pdf")
# Parallel batch conversion
with Epub2Pdf(engine="playwright") as client:
reports = client.batch_convert(
[("a.epub", "a.pdf"), ("b.epub", "b.pdf")],
max_workers=4,
)
MCP (Claude Desktop / Claude Code)
Install the optional MCP dependency:
python3 -m pip install -e '.[mcp]'
Add the server to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%/Claude/claude_desktop_config.json on Windows):
{
"mcpServers": {
"epub2pdf": {
"command": "epub2pdf-mcp"
}
}
}
If epub2pdf-mcp is not on your PATH, use the absolute path to your Python interpreter:
{
"mcpServers": {
"epub2pdf": {
"command": "/path/to/python3",
"args": ["-m", "epub2pdf_cli.mcp_server"]
}
}
}
The MCP server uses low-resource defaults (WeasyPrint, no PDF validation, no long-lived browser) and exposes these tools:
convert_epubbatch_convertinspect_epubextract_pdf
Codex and OpenCode Usage
After running:
python3 scripts/install_agent_skills.py
Codex and OpenCode can use the global epub2pdf skill.
Example prompts:
Use $epub2pdf to inspect this EPUB and convert it to a searchable PDF with JSON and HTML sidecars.Use $epub2pdf to extract Markdown and JSON from this PDF for RAG ingestion.Use $epub2pdf to inspect the EPUB TOC before rendering the PDF.
The skill should call epub2pdf inspect, epub2pdf convert, or epub2pdf pdf-extract directly.
Guarantees
- Normal XHTML-based EPUB text is rendered as a real PDF text layer, not as raster-only pages.
- Rendered PDFs are validated for page count and extractable text when textual source chapters exist.
- EPUB sidecars are produced from the source EPUB before PDF rendering, so AI workflows do not need to reverse-engineer the PDF.
- PDF extraction is local and deterministic by default.
Limitations
- Fixed-layout comics and image-only EPUBs are rendered, but not OCR-processed.
- Scanned PDFs that require OCR need a separate workflow; the local
pdf-extractcommand does not start the hybrid OCR backend unless Docling is used. - Complex EPUB CSS may be simplified during normalization.
WeasyPrintis the default backend and may require native system libraries (Pango, Cairo, GDK-PixBuf).pdf-extractno longer requires Java by default; Java is only needed for the legacyopendataloaderengine.
Development
For detailed setup instructions, see docs/development/setup.md.
Quick start:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -e '.[all,dev]'
PYTHONPATH=src python3 -m pytest -q
Before committing, run all checks:
python3 -m ruff check src tests
python3 -m mypy src
PYTHONPATH=src python3 -m pytest -q --cov=epub2pdf_cli --cov-fail-under=60
The source is organized into layered packages:
epub2pdf_cli/cli.py— argument parsing and command dispatch only.epub2pdf_cli/config.py— validated configuration objects.epub2pdf_cli/epub/— EPUB container, OPF, TOC, and chapter parsing.epub2pdf_cli/html/— HTML normalization, asset rewriting, CSS rewriting, and template generation.epub2pdf_cli/render/— PDF rendering engines behind aRendererprotocol.epub2pdf_cli/pdf/— PDF validation and extraction adapters.epub2pdf_cli/pipeline/— high-levelinspect,convert, andextractworkflows.
Examples are under examples/:
batch_convert.sh— convert all EPUBs in a directory.custom_renderer_plugin.py— skeletonRendererimplementation.custom_extractor_plugin.py— skeletonExtractorimplementation.
Refresh global skill wrappers after editing integration templates:
python3 scripts/install_agent_skills.py
Community
- GitHub Discussions for questions and ideas.
- docs/community/discussions.md for the onboarding flow and category guide.
- docs/community/operation.md for triage, release rhythm, and maintainer criteria.
- docs/community/releases.md for the release checklist.
- docs/community/triage.md for issue and PR triage.
Contributing
We welcome bug reports, feature requests, documentation fixes, and pull requests.
- Read CONTRIBUTING.md for development setup and PR standards.
- Read CODE_OF_CONDUCT.md before participating.
- Report security issues privately via SECURITY.md.
- See ROADMAP.md for planned work and GOVERNANCE.md for project roles.
Repository
Public GitHub repository:
https://github.com/min9lin9/epub2pdf
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file epub2pdf_cli-0.3.4.tar.gz.
File metadata
- Download URL: epub2pdf_cli-0.3.4.tar.gz
- Upload date:
- Size: 42.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
335b62fc754cba3282a4978ae8ac04e7b41630a200dda4d569e19cb4fabeba39
|
|
| MD5 |
1dc0a08ed8fd82645c0837ae6a086b68
|
|
| BLAKE2b-256 |
1a6746374a8afd2393b5bfcc89a34361f9af81afe42a39edddd2666cde04abf5
|
Provenance
The following attestation bundles were made for epub2pdf_cli-0.3.4.tar.gz:
Publisher:
release.yml on min9lin9/epub2pdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
epub2pdf_cli-0.3.4.tar.gz -
Subject digest:
335b62fc754cba3282a4978ae8ac04e7b41630a200dda4d569e19cb4fabeba39 - Sigstore transparency entry: 1822900700
- Sigstore integration time:
-
Permalink:
min9lin9/epub2pdf@56ac06ec58e4a4109247fdc7b221e25c42e4c28b -
Branch / Tag:
refs/tags/v0.3.4 - Owner: https://github.com/min9lin9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@56ac06ec58e4a4109247fdc7b221e25c42e4c28b -
Trigger Event:
push
-
Statement type:
File details
Details for the file epub2pdf_cli-0.3.4-py3-none-any.whl.
File metadata
- Download URL: epub2pdf_cli-0.3.4-py3-none-any.whl
- Upload date:
- Size: 53.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b437e4e01bb273b3fd37d59ec1fdc92e07ba3144915ad779f58c05974137432
|
|
| MD5 |
5b1188ba9f0fa23304b77e671bbf0267
|
|
| BLAKE2b-256 |
3a918a29e10039f2a558c86b47ebdc6403e5a944bb47ef79e7c42d0ba72dfd02
|
Provenance
The following attestation bundles were made for epub2pdf_cli-0.3.4-py3-none-any.whl:
Publisher:
release.yml on min9lin9/epub2pdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
epub2pdf_cli-0.3.4-py3-none-any.whl -
Subject digest:
1b437e4e01bb273b3fd37d59ec1fdc92e07ba3144915ad779f58c05974137432 - Sigstore transparency entry: 1822900876
- Sigstore integration time:
-
Permalink:
min9lin9/epub2pdf@56ac06ec58e4a4109247fdc7b221e25c42e4c28b -
Branch / Tag:
refs/tags/v0.3.4 - Owner: https://github.com/min9lin9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@56ac06ec58e4a4109247fdc7b221e25c42e4c28b -
Trigger Event:
push
-
Statement type: