Turn a PDF book into a clean, reflowable EPUB 3 — and fix EPUBs that refuse to open.
PDFs are frozen paper pages; e-readers want flowing text. Naive conversion drags the paper layout along — running headers and page numbers in the middle of paragraphs, words hyphen-ated across line breaks, chapters the reader can't navigate. unbind extracts the text with PyMuPDF4LLM, cleans all of that up, and builds the EPUB with Pandoc:
$ unbind book.pdf
PDF type: text_based (0 of 312 pages need OCR)
Extracting Markdown from: book.pdf
Removed 618 running header, footer, or page-number lines.
Creating EPUB: book.epub
Created successfully: book.epubFor the longer story — what goes wrong in a naive conversion and how unbind handles it — see the project write-up.
- Chapters from PDF bookmarks — bookmarks become the authoritative heading
hierarchy: detected headings are re-leveled, headings are inserted for
image-only chapter pages, and false positives (like the book's printed table
of contents) are demoted.
--ignore-pdf-tocopts out. - Page-artifact cleanup — lines that repeat at the top or bottom of many
pages (running titles, bare page numbers) are removed; real headings and
images are never touched.
--no-cleanupopts out. - Hyphenation repair — words split across line and page breaks are rejoined.
- Metadata and cover for free — title/author come from the PDF metadata
(fallback: filename), and the first page is rendered as the cover.
Override with
--title/--author/--cover, or--no-cover. - Scanned books, recognised automatically — unbind checks every page, detects the script the pages are printed in, and OCRs the ones that need it, including pages whose existing OCR layer was recognised in the wrong language (the usual cause of mojibake output). Pages are recognised in parallel. See below.
- Batch conversion — point it at a directory;
--jobs Nconverts books in parallel, failures are reported and skipped, and the exit code tells the truth. - EPUB check and repair — diagnose and fix EPUBs that readers refuse to open. See below.
- Inspectable intermediate —
--keep-markdownretains the extracted Markdown and images for hand-editing.
Every converted book is verified as a well-formed EPUB before unbind reports success.
Requires Python 3.12+ and Pandoc. Scanned books additionally need Tesseract with the language data for the book — unbind names the exact package if it is missing.
git clone https://github.com/Han8931/unbind
cd unbind
uv sync
uv run unbind --helpuv run unbind book.pdf
uv run unbind book.pdf -o output/ --title "Book title" --author "Author"
uv run unbind scanned.pdf
uv run unbind book.pdf --keep-markdownThe output defaults to the input filename with an .epub extension; passing a
directory to -o places the EPUB inside it. --force overwrites existing
output, --toc-depth N controls table-of-contents depth (default 2), and
--language sets the book's language code (default: the script detected during
OCR, otherwise en). --link-footnotes (experimental) turns superscript
markers into linked EPUB footnotes when the footnote text is found on the same
page.
Nothing extra to pass — unbind scanned.pdf is the whole command:
$ unbind zorba.pdf
OCR: 496 pages in 'kor' (the existing text is only 0% Hangul though the pages are)
Recognising pages takes a few seconds each; this is the slow part.
OCR: 496/496 pages
Extracting Markdown from: zorba.pdf
Creating EPUB: zorba.epub
Created successfully: zorba.epubunbind classifies every page as blank, scan, existing-OCR-layer, or
born-digital text. Scans with no text are always recognised. Pages that already
carry an OCR layer are re-recognised only when that layer disagrees with the
script Tesseract sees in the page images — the failure mode that produces
3] x 7}O|A} 20A]|7] where the page plainly reads 그리스인 조르바. Born-digital
books are never rendered or recognised, so they cost nothing.
The detected script also sets the EPUB's language, so the reader hyphenates and
line-breaks correctly without --language.
| Flag | Effect |
|---|---|
| (none) | Recognise the pages that need it; keep good text as-is |
--ocr |
Recognise every page, discarding any text the PDF carries |
--no-ocr |
Never recognise; extract the existing text layer as-is |
--ocr-language kor |
Skip script detection and use this Tesseract language (kor+eng also works) |
--ocr-jobs N |
Pages to recognise in parallel; defaults to every core but one |
Missing language data is reported before any work starts, with the package to install for your distribution.
Passing a directory converts every PDF in it (non-recursively):
uv run unbind pdfs/
uv run unbind pdfs/ -o epubs/ --jobs 4
uv run unbind pdfs/ --keep-markdown-o names the output directory (default: the input directory). Each book's
title and author come from its own metadata or filename, so --title,
--cover, and --markdown-output are rejected in batch mode; everything else
applies to every file. --ocr-jobs is divided among the books running in
parallel, so the machine is not oversubscribed.
Some EPUBs simply won't open — and the reader never tells you why. Pass an
.epub to unbind and it will:
$ unbind book.epub
book.epub has problems that can keep readers from opening it:
- content file fails strict XML parsing: OEBPS/Text/page-13.xhtml
(undefined entity : line 13, column 5)
Notes:
- EPUB version 2.0
Re-run with --repair to attempt a fix.
$ unbind book.epub --repair
...
Repaired EPUB written to: book.repaired.epubThe check covers the container (mimetype entry placement and compression,
container.xml, package manifest, missing resources) and every content
document, parsed the same strict way reader apps parse XML — which catches the
two classic killers: named HTML entities like , legal under the XHTML
DTD but undefined to a parser that never loads it, and bare void tags like
<br>, valid HTML but not valid XML.
Either one makes a reader fall back to lenient HTML recovery for every file in the spine, so a book that still opens can take far longer to index than its size suggests.
--repair writes a fixed copy next to the original (never modifying it):
the archive is repackaged correctly, named entities become numeric character
references ( →  , identical rendering), bare void tags are
closed (<br> → <br/>), stray HTML pasted into a stylesheet is stripped,
duplicate entries are dropped, and a missing container.xml is regenerated
when possible. Text is never altered.
The report ends with brief notes on what repair cannot change: DRM encryption (a DRM-locked book only opens in a reader authorized for it — unbind does not remove DRM), font obfuscation (harmless), and the EPUB version. For a full specification audit beyond "will it open," use epubcheck.
unbind is built for books — prose with chapters. Multi-column academic papers and heavily designed magazines are out of scope, and heading detection is only as good as the PDF's bookmarks and typography.
uv sync
uv run pytest
uv run ruff check src tests
uv run ruff format src testsCI runs the linter and tests on Python 3.12 and 3.14.
MIT — see LICENSE.