Skip to content

Repository files navigation

epub2pdf

CI License: MIT Node

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

Download

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.

Get it running

The executable — nothing to install

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.

From source

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.epub

To rebuild the executable after changing the sources:

npm run build

Usage

epub2pdf <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

Options

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.

Use as a library

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.

How it works

  1. Read the archive (src/zip.js) — a small ZIP reader built on node:zlib, with Zip64 and CRC checks.
  2. 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.
  3. 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.
  4. 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, then Page.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 desktop window

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.

Tests

npm test

The 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

Limitations

  • 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 --toc are 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 build produces a native binary on macOS and Linux too, and the CLI runs from source anywhere Node 22 and Chrome do.

Contributing

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.

Licence

MIT © AFRA

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages