Convert EPUB books to PDF — from a desktop window or the command line. The book's own stylesheets, fonts and images are kept, and Chrome does the page layout, so the result looks like the book rather than a text dump.
Ships as a single executable with one dependency: no Electron, no Puppeteer, no bundled browser.
epub2pdf book.epub
Grab the latest Windows build from the
releases page,
unzip it anywhere, and double-click epub2pdf.vbs.
You need a Chrome, Chromium or Edge installed — that is what renders the pages. Edge ships with Windows, so this is normally already satisfied.
The release zip holds a standalone Windows app. No Node, no npm, no
node_modules. Keep the three files together:
epub2pdf.vbs— double-click for the app window, with no console behind it. This is the one to make a shortcut to.epub2pdf.exe— the program itself. Double-clicking it also opens the window (with a console visible), and it is the command-line tool.Drop EPUBs here.cmd— drag EPUBs onto it to convert them without opening the window at all.
In the window: drop in one or more EPUBs, pick page size, margins, text size and what to include, then hit Convert. Each book gets a Save PDF button, and a single book saves straight away.
If your browser lives somewhere unusual, point at it with --browser <path>.
Windows may warn about an unrecognised publisher the first time you run it. The binary is unsigned: injecting the app into the Node runtime invalidates Node's own signature. Each release publishes SHA-256 checksums you can verify against.
Requires Node 22 or newer (the renderer uses the global WebSocket) and a
Chrome-family browser.
git clone https://github.com/afradigm/epub2pdf.git
cd epub2pdf
npm install
node bin/epub2pdf.js book.epubTo rebuild the executable after changing the sources:
npm run buildepub2pdf <book.epub> [more.epub ...] [options]epub2pdf book.epub # writes book.pdf alongside it
epub2pdf book.epub -o ~/Desktop/book.pdf --toc --page-numbers
epub2pdf *.epub -d ./pdfs --format Letter --font-size 12pt
epub2pdf book.epub --html -o out/book.html # inspect the assembled HTML| Option | Description |
|---|---|
-o, --output <file> |
Output file (single input only) |
-d, --out-dir <dir> |
Write outputs into this directory |
--html |
Emit the assembled HTML instead of a PDF |
--format <size> |
A4 (default), A3, A5, A6, Letter, Legal, Tabloid |
--landscape |
Landscape orientation |
--margin <css> |
Margin for all sides, e.g. 20mm |
--margin-top/-right/-bottom/-left <css> |
Per-side margins |
--scale <n> |
Render scale, 0.1–2 (default 1) |
--prefer-css-page-size |
Honour @page size declared by the book's CSS |
--font-family <css> |
Override the body font, e.g. "Georgia, serif" |
--font-size <css> |
Base font size, e.g. 12pt |
--line-height <n> |
Base line height, e.g. 1.5 |
--css <text|file> |
Extra CSS, inline or a path to a .css file |
--no-epub-styles |
Ignore the book's own stylesheets |
--toc |
Insert a generated, linked table of contents |
--no-cover |
Skip the cover image page |
--no-title-page |
Skip the generated title page |
--no-chapter-breaks |
Do not force a page break between documents |
--page-numbers |
Print page numbers in the footer |
--page-x-of-y |
Print n / total page numbers |
--header <text> |
Repeat this text in the page header |
--browser <path> |
Chrome/Edge executable to render with |
--timeout <ms> |
Render timeout (default 120000) |
--keep-temp |
Keep the extracted working directory |
-q, --quiet |
Only print errors |
A cover image, when the book has one, replaces the generated title page. Page numbers are drawn inside the bottom margin, so epub2pdf widens that margin to 20mm unless you set it yourself.
--html writes name.html plus a name_files/ folder of assets, linked with
relative paths so the pair can be moved together.
import { convertEpubToPdf, readEpub } from './src/index.js';
const { epub } = await convertEpubToPdf('book.epub', 'book.pdf', {
toc: true,
pageNumbers: true,
format: 'A5',
});
console.log(epub.metadata.title, epub.spine.length);readEpub(path) on its own gives you the parsed metadata, manifest, spine and
table of contents without rendering anything.
- Read the archive (
src/zip.js) — a small ZIP reader built onnode:zlib, with Zip64 and CRC checks. - Parse the package (
src/epub.js) —META-INF/container.xml→ the OPF package → metadata, manifest and spine, plus the table of contents from either the EPUB 3 nav document or the EPUB 2 NCX. - Assemble one HTML file (
src/html.js) — every spine document is extracted to a temp directory and concatenated. Ids are namespaced per chapter so repeated ids cannot collide, cross-document links become in-page anchors, and every relative asset URL is rewritten to the extracted file. - Print it (
src/render.js,src/cdp.js) — an installed Chrome is launched headless and driven straight over the DevTools protocol: navigate, wait for fonts and images, thenPage.printToPDF, streamed back in chunks. Speaking CDP directly instead of going through Puppeteer is what keeps this to one dependency, and small enough to ship as a single executable.
Scripts and inline event handlers are stripped before rendering: they are never needed for print output, and EPUB files are untrusted input.
The same trick, turned outward. src/server.js serves the interface
(src/ui.js) on a random localhost port behind a token generated at startup, and
src/gui.js opens Chrome in app mode against it — a plain window with no tabs or
address bar, running from a throwaway profile so closing it ends the program.
That is how the app gets a real UI while staying a single binary with no UI
framework attached.
npm testThe suite builds a synthetic EPUB fixture (nested directories, a shared
stylesheet, ../ asset references, colliding chapter ids, legacy <a name>
anchors, a nav TOC and a linear="no" spine entry), then asserts on the parsed
structure, the assembled HTML and the page count of real generated PDFs.
To produce that fixture for manual poking:
npm run fixture- No PDF bookmarks. Chrome's print pipeline cannot emit an outline, so the
book's TOC becomes a linked contents page (
--toc) rather than a PDF sidebar. - Fixed-layout EPUBs (comics, illustrated children's books) are detected and scaled to fit the page, but their per-page geometry is not reproduced exactly.
- DRM-protected books are not supported and never will be; they will fail to parse.
- Page numbers in a generated
--tocare not available, since page breaks are only known after rendering. - Prebuilt binaries are Windows-only so far. The code itself is
cross-platform —
npm run buildproduces a native binary on macOS and Linux too, and the CLI runs from source anywhere Node 22 and Chrome do.
Bug reports and pull requests are welcome — see CONTRIBUTING.md for setup, the test suite and what makes a report useful. Security issues have their own process in SECURITY.md.
MIT © AFRA