Convert PDF books in right-to-left languages (Persian first) into reflowable EPUB 3 for reading apps built on Readium, such as Thorium Reader. It supports PDFs exported from a word processor. Scanned books are read with ParisaOCR's model and work well on the one scan measured so far; more kinds of print are on the roadmap. How it works: docs/DESIGN.md.
Note: This project was designed and written by Claude Opus 5.5 (Anthropic) in Claude Code. See Credits.
# Build the Docker image (make build; `make` lists the other commands)
docker build -f docker/Dockerfile -t rtlbook:dev .
# Create input/ and output/, then put your PDF in input/
./rtlbook init
# Exported PDF or scan? Right script?
./rtlbook inspect input/book.pdf
# Convert to EPUB → output/book.epub
./rtlbook convert input/book.pdf --title "…" --author "…"./rtlbook runs the CLI in Docker. Only this repository folder is shared with the container,
so the tool can't see or change anything else on your machine. Input PDFs must therefore be inside the
repo. Copy them into input/, since a path like ~/Downloads/book.pdf isn't visible to it. Paths work
the same inside and outside the container, relative or absolute, including when you run from a
subfolder. Running it from outside the repo stops with an error. Your local src/ is mounted too, so
code changes apply without a rebuild.
What has been verified:
- 8 Persian novels (3,231 pages, born-digital PDFs from Word, pdfFactory, iText and others) converted to EPUB 3 that passes epubcheck.
- Apple Silicon Mac (M2) with Docker Desktop.
Not yet done or known to be weak:
- Scanned books are measured on few books so far: on hand-checked pages of a 1932 letterpress scan, 0.3% of letters are misread (Tesseract alone: 6%); on a 1963 printing and three modern editions, where the two readers disagree, ParisaOCR was judged right far more often. Typewriters aren't measured; ornamental frames and very close lines can still turn into junk lines; and a scan takes about five times as long as an exported PDF (about 5–6 s a page on one core).
- Accuracy is measured on few books so far: one exported story against its own stored text, and two
hand-checked pages of a scanned book (
tests/data/). - Persian only: Arabic, Urdu and Hebrew are on the roadmap, not built.
- Known OCR quirks: in some bold fonts,
!without a following space is read asا, which merges words. Front pages (catalogue records, site banners) can come out as junk text. - Titles and authors are read from the title page when it has them in text; check what
convertreports and pass--title/--authorwhen it's wrong or missing. - No web app yet. The CLI is the only interface.
See DESIGN.md §11–13 for the dated decisions and measurements, and ROADMAP.md for what's next. Practical lessons and troubleshooting (OCR, EPUB, Docker on macOS) are in LESSONS.md.
| Folder | What goes there | In git? |
|---|---|---|
input/ |
PDFs to convert. Copy them here, because the container can only see the repo folder | No, fully ignored |
output/ |
Results: <name>.epub, plus <name>.rtlbook/, the per-book work folder |
No, fully ignored |
output/<name>.rtlbook/ |
OCR cache (pages/*.json), pages.txt, paragraphs.txt, report.json, epubcheck.txt |
No |
./rtlbook init creates both folders (convert also creates output/ if it's missing). Books can be
copyrighted or private, so the folders and everything in them are git-ignored and never committed.
convert writes to output/<pdf name>.epub unless you pass -o. It reuses the OCR cache in
output/<name>.rtlbook/, so re-running with different text or EPUB options takes seconds.
Convert a whole folder:
for pdf in input/*.pdf; do
./rtlbook convert "$pdf"
doneUseful convert options:
| Option | Purpose |
|---|---|
--digits keep |
Keep digits as printed (default auto: Persian ۰–۹ for fas) |
--pages 1-120 |
Convert part of a book |
-j 8 |
Pages OCR'd in parallel (default: all CPUs) |
--progress json |
Progress as JSON lines on stderr (start, one page per page, done), for other programs |
--crop 0.075 |
Cut page frames/borders before OCR |
--drop-lines REGEX |
Remove watermark or banner lines |
--min-line-conf 40 |
Drop OCR lines below this confidence |
--reader tesseract |
Read a scan's lines with Tesseract instead of ParisaOCR: five times faster, many more letters wrong (default auto: a scan's lines cut and read one by one by ParisaOCR, an exported PDF's whole pages by Tesseract; page: whole pages in one Tesseract call) |
--scan-reader tesseract |
With --reader auto, read a scan's lines with Tesseract instead of ParisaOCR; exported PDFs keep Tesseract's whole-page call |
--no-split-joined |
Keep words run together as read (scans only; see below) |
--no-cover |
No cover page or cover image |
--lines SET |
Build from a checked set of lines (rtlbook lines' format, docs/TRAINING-DATA.md) instead of OCR |
--no-half-spaces |
Keep the spaces before endings and after prefixes as read (see below) |
-o PATH |
Output EPUB (default output/<pdf name>.epub) |
--work DIR |
Work/cache folder (default output/<name>.rtlbook/). Re-runs reuse per-page OCR results |
What convert does automatically:
- OCRs every page. The text stored inside Persian PDFs is rarely usable as is: wrong character codes, words in reversed (visual) order, missing spaces. On an exported story whose stored text could be rebuilt, OCR still got over 99% of letters right.
- Cleans up OCR: Persian letters and digits, commas read as
»/ء, mirrored parentheses, stray marks, and running headers/watermarks repeated on many pages. - Reads a scan's lines with ParisaOCR's model (Tesseract finds the lines); an exported PDF's pages with Tesseract.
- Splits words run together in scans of old type, where the gap after a letter that doesn't join the next is a hair wide (
ورفتم→و رفتم), using word lists;report.jsoncounts them (joined_words_split). - Puts half-spaces where today's spelling has them, before the plural
ها,تر/ترینand a few endings, and after the prefixesمی/نمیandبی(می رفت→میرفت), so the word doesn't break across lines and search finds it;report.jsoncounts them (half_spaces_made). - Records how the book was made in the EPUB's metadata and
report.json(engine): rtlbook's version, scan or exported, and which readers found and read the lines. - Rebuilds paragraphs, including dialogue lines (
سارا- …,علی : …), and joins them across page breaks. - Detects chapters even when OCR misreads the heading (
نصل دو از دهم→فصل دوازدهم). Books without chapters get ~20-page sections. - Builds a valid EPUB 3 (epubcheck) with RTL page direction, the Parastoo font, cover, and print page list.
Typical real-world run (site credits on the title pages, extra front pages):
./rtlbook convert input/Gandom.pdf --title "گندم" --author "م. مودبپور" \
--drop-lines 'کتابخانه مجازی|تهیه و تنظیم' --pages 2-557output/<name>.rtlbook/ keeps pages/*.json (per-page OCR with boxes and confidence),
pages.txt, paragraphs.txt, report.json and epubcheck.txt for review.
eval compares OCR output with a correct text of the same pages, e.g. hand-corrected. Spelling conventions that differ between
an old printing and a modern transcription (Arabic ي/ك, digits, half-spaces, vowel marks) count as
equal.
./rtlbook eval output/book.rtlbook output/ref.txt --pages 24 --diff output/diff.txtIt reports the character error rate (CER, spaces ignored), the word error rate (WER), how many reference words appear anywhere in the OCR text, and a CER that matches each reference line to its closest OCR line. When the last two are much better than the first, the words were read correctly but in the wrong order, which is a layout problem (e.g. two-column verse) rather than misread letters. A reference longer than the pages (a whole chapter for a page that shows only its end) is trimmed to the part the OCR covers.
lines cuts pages of a book into one image per text line, with what OCR read and its uncertain words, for a person
to check; the checked lines become ground truth and training data for the line readers. The format, the rules for checking
and how test lines are kept out of training: docs/TRAINING-DATA.md.
./rtlbook lines input/book.pdf --pages 30,54 --split test --out lines/book --common words.txt
./rtlbook lines input/book.pdf --pages 1-29,31-53 --split train --out lines/book --common words.txtOpen the EPUB in Thorium Reader (Windows, macOS, Linux) or another app built on Readium.
make test, or:
docker run --rm -v "$PWD/src":/app/src:ro -v "$PWD/tests":/app/tests:ro -w /app \
--entrypoint python rtlbook:dev -m pytest -q -p no:cacheprovider testsGitHub Actions (.github/workflows/ci.yml) runs them on every push and pull request, in the image built for
linux/amd64, and fails if a PDF, EPUB, scan or anything in input//output/ has been committed.
rtlbook version prints the version; releases are git tags named vX.Y.Z. When the version changes, how to
release, and what changed in each version: docs/VERSIONS.md.
- Code and design: written by Claude Opus 5.5 (Anthropic) using Claude Code, with the project maintainer supplying the test books, checking the outputs, and making the product decisions (Docker, local-only, EPUB only).
- Built on: Tesseract OCR and its
tessdata_bestmodels (Apache-2.0) · PDFium via pypdfium2 · W3C EPUBCheck · Parastoo font by Saber Rastikerdar (SIL OFL 1.1), embedded in the EPUBs · Lilak, a Persian spelling dictionary by Mostafa Sedaghat Joo (Apache-2.0), whose word list splits joined words · ParisaOCR by Ali Javadi (Apache-2.0), whose recognition model reads scans, run by ONNX Runtime (MIT) · Kraken by Benjamin Kiessling (Apache-2.0), whose bidi algorithm puts ParisaOCR's output in reading order.
Apache-2.0. The public-domain ground-truth texts in tests/data/ are not ours and not covered by the
license (see the README in each folder). The tools rtlbook runs (Tesseract and its models, ParisaOCR's model,
ONNX Runtime, epubcheck), the Parastoo font and Lilak's word list keep their own licenses; none of them is stored in
this repository. src/rtlbook/bidi/ is Kraken's code with tables from Unicode's data, and readers.py follows
ParisaOCR's code for its model: see NOTICE.