Convert PDFs into clean reflowable EPUBs on macOS. Built with Apple's PDFKit, Vision and Foundation Models to scan, format and clean up PDFs and turn them into EPUB files. Works on scans and transcriptions.
Just want to convert some books? Follow the Quick start below. No Terminal needed. Developers can build from source.
Requirements: macOS 26 or later on Apple Silicon. Apple Intelligence is optional. Apple's foundation model is used for a proofreading/formatting pass. Should run fine without this.
PDF-to-EPUB-<version>.pkg from the
Releases page (under Assets)..pkg and click through the installer. It asks for your
Mac's password because it installs a small command-line tool for all users..epub is
saved next to the original PDF.Good to know:
pdf2epub command for Terminal users. See Usage for
its options.Scripts/uninstall.sh (also handy to clean up before reinstalling).
It removes /usr/local/bin/pdf2epub, ~/bin/pdf2epub, /Library/Application Support/PDF to EPUB, ~/Library/Services/Convert to EPUB.workflow, and the installer's receipt.Requires Xcode (or its Command Line Tools) with the macOS 26 SDK. See Development for tests and debugging.
Install for yourself: Scripts/install.sh builds the tool, installs it to ~/bin, and
installs the Quick Action into ~/Library/Services. Then enable the Quick Action as in step 3
of the Quick start.
Making a release: Scripts/release.sh builds, signs (Developer ID, hardened runtime),
packages, notarizes, and staples dist/PDF-to-EPUB-<version>.pkg. The version comes from
pdf2epub --version. One-time setup: create Developer ID Application and Developer ID
Installer certificates (Xcode → Settings → Accounts → Manage Certificates), then store
notarization credentials with
xcrun notarytool store-credentials pdf2epub-notary --apple-id <you> --team-id <TEAMID>,
using an app-specific password from appleid.apple.com.
Without a Developer ID: run Scripts/package.sh and share dist/PDF-to-EPUB.zip. The person
unzips it and double-clicks Install.command, then right-clicks a PDF and chooses
Quick Actions → Convert to EPUB. The EPUB appears next to the PDF, followed by a
notification. The binary is signed ad hoc, not notarized, so if it was downloaded macOS
blocks it the first time: open System Settings → Privacy & Security and click
Open Anyway.
pdf2epub book.pdf # writes book.epub next to it
pdf2epub <input.pdf> [-o output.epub]
[--profile generic|xteink] # default: generic
[--ocr auto|always|never] # default: auto
[--no-llm] # skip the Foundation Models pass
[--title "…"] [--author "…"]
[--verse] # keep line breaks in short-line passages
[--keep-intermediate <dir>] # per-stage JSON + the model transcript
[--progress-window] # a progress bar window (the Quick Action uses it)
[--verbose]
Exit status is 0 on success, 1 on failure, and 2 on success with warnings (for example,
low-confidence OCR pages, or passages the model declined to review). Progress
(page 37/212) goes to stderr, or to a small window with --progress-window.
When the PDF has no title or author, they come from a download-style file name
(Title _ Subtitle -- Last, First_.pdf) or from the title page.
Profiles:
xteink: for the Xteink X4 on CrossPoint-family firmware. It emits a restricted
XHTML subset, a CSS file under 2 KB, chapter files of at most 64 KB, grayscale baseline
JPEGs no wider than 480 px, and a 480×800 cover.generic: for Kindle, Kobo, Apple Books, and KOReader. Limits are relaxed: color
images up to 1200 px wide and no file splitting.PDF ─► Ingest ─► Layout heuristics ─► LLM review ─► Structure ─► EPUB
text layer headers/folios narrow yes/no chapters EPUB 3 + NCX
or OCR columns, verse questions only footnotes own zip writer
paragraphs, joins figures
Sources/PDF2EPUBCore/Ingest): PDFKit's attributed strings report every font
as "Helvetica", so GlyphScanner walks the content stream with CGPDFScanner to recover
each glyph's real font (italic, bold, small caps), size, and position. It then matches them
to PDFKit's characters. A scanner's invisible OCR layer is kept when it carries real
typography (several fonts, italics: professional OCR, which also marks superscript note
numbers) and reads well. A page goes to Vision OCR when it has no text layer, a plain
single-font hidden layer (Internet Archive style), or text that mostly fails the spell
checker.OCR/VisionOCR.swift): RecognizeDocumentsRequest, with RecognizeTextRequest
as a fallback, pinned to the book's language (auto-detection turns old-style numerals into
Cyrillic). Tables are cropped as images. OCR'd text then gets deterministic repairs
(Cleanup/): small caps read as mixed case (SeTTLeMENTS → SETTLEMENTS), Cyrillic
look-alikes, fullwidth punctuation, and glyph mis-mappings learned from the book itself
(one layer's italic "k" came out as "").Layout/): running heads and folios (by repetition, position, and page-number
sequence); two-column reading order; paragraphs (by indent, gaps, and short last lines);
verse (capitalized lines, the fill test, alternating indents); headings; footnotes;
dehyphenation that follows the book's own spelling ("to-day" stays if the book writes
it); and joins across page breaks.Cleanup/): only for the cases the heuristics flag. See below.Structure/, EPUB/): chapters come from the PDF outline; else
from the book's printed Contents page, placed through the printed page numbers (this
works even when chapter titles are display art); else from detected headings. Footnotes
become linked notes in the same file; references to a Notes chapter link to the right
endnote and back. Figures and captions are paired. Packaging uses its own zip writer, with mimetype
stored first.Foundation Models works from a plain CLI binary and from the Quick Action. No app bundle or entitlements are needed.
Guardrails: the default model refuses violent classical passages (the Iliad). The
SystemLanguageModel(guardrails: .permissiveContentTransformations) mode accepts them,
but only for plain String output. Guided @Generable output still refuses, so the
pass asks for short string answers and parses them itself.
Vision: RecognizeDocumentsRequest text is excellent, but its paragraph grouping
is unreliable around dialogue, and its line heights vary by about ±30%. OCR'd pages are
therefore segmented by geometry, and size only counts when the difference is large.
The ~3B model can't follow a multi-block reply format and likes to "help" by modernizing (unnumber'd → unnumbered, ancle → ankle). So every request is one narrow question:
A spelling change is accepted only if it turns a non-word into a word through a typical OCR confusion (rn↔m, tl↔d, c↔e). Digits, elisions, diacritics and ligatures are never removed, and names are left alone. With these rules, the pass is never worse than heuristics alone on the test fixtures. It asks at most 400 questions per book.
swift build -c release
swift test
If xcode-select points at an older Command Line Tools install, prefix commands with
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer. The Scripts do this
automatically.
Scripts/make-fixtures.swift typesets public-domain excerpts from
Tests/PDF2EPUBCoreTests/Fixtures/sources (Pride and Prejudice, Pope's Iliad) into
book-like PDFs. These have running heads, folios, hyphenation, italics, and verse with
turnovers. It also makes scanned and junk-OCR-layer variants, plus the expected text.
The golden tests require exact paragraph-for-paragraph output on the born-digital PDFs,
and a minimum match ratio on the scans.java -jar epubcheck.jar book.epub. Every corpus book currently passes with 0 errors and
0 warnings.swift Scripts/excerpt-pdf.swift book.pdf excerpt.pdf 1-60,296-305 copies
chosen pages (text layer included) into a smaller test PDF. Keep local test books in
TestCorpus/, which git ignores.--keep-intermediate dir writes blocks-heuristic.json, blocks-cleaned.json,
chapters.json, and llm-transcript.txt.--dump-page N [--ocr always] prints one page's extracted lines with geometry and
styles.PDF2EPUB_DEBUG_VERSE=1 prints the verse-detection statistics for each region.Claude Opus 5.5 made all of this. I take no credit beyond the initial desire and some busy work to bundle it up. It's MIT license so I also take no legal responsibility. I found this to be a useful tool. Hopefully this tool will be useful to you.
Convert PDFs into clean reflowable EPUBs on macOS. Built with Apple's PDFKit, Vision and Foundation Models to scan, format and clean up PDFs and turn them into EPUB files. Works on scans and transcriptions.
Just want to convert some books? Follow the Quick start below. No Terminal needed. Developers can build from source.
Requirements: macOS 26 or later on Apple Silicon. Apple Intelligence is optional. Apple's foundation model is used for a proofreading/formatting pass. Should run fine without this.
PDF-to-EPUB-<version>.pkg from the
Releases page (under Assets)..pkg and click through the installer. It asks for your
Mac's password because it installs a small command-line tool for all users..epub is
saved next to the original PDF.Good to know:
pdf2epub command for Terminal users. See Usage for
its options.Scripts/uninstall.sh (also handy to clean up before reinstalling).
It removes /usr/local/bin/pdf2epub, ~/bin/pdf2epub, /Library/Application Support/PDF to EPUB, ~/Library/Services/Convert to EPUB.workflow, and the installer's receipt.Requires Xcode (or its Command Line Tools) with the macOS 26 SDK. See Development for tests and debugging.
Install for yourself: Scripts/install.sh builds the tool, installs it to ~/bin, and
installs the Quick Action into ~/Library/Services. Then enable the Quick Action as in step 3
of the Quick start.
Making a release: Scripts/release.sh builds, signs (Developer ID, hardened runtime),
packages, notarizes, and staples dist/PDF-to-EPUB-<version>.pkg. The version comes from
pdf2epub --version. One-time setup: create Developer ID Application and Developer ID
Installer certificates (Xcode → Settings → Accounts → Manage Certificates), then store
notarization credentials with
xcrun notarytool store-credentials pdf2epub-notary --apple-id <you> --team-id <TEAMID>,
using an app-specific password from appleid.apple.com.
Without a Developer ID: run Scripts/package.sh and share dist/PDF-to-EPUB.zip. The person
unzips it and double-clicks Install.command, then right-clicks a PDF and chooses
Quick Actions → Convert to EPUB. The EPUB appears next to the PDF, followed by a
notification. The binary is signed ad hoc, not notarized, so if it was downloaded macOS
blocks it the first time: open System Settings → Privacy & Security and click
Open Anyway.
pdf2epub book.pdf # writes book.epub next to it
pdf2epub <input.pdf> [-o output.epub]
[--profile generic|xteink] # default: generic
[--ocr auto|always|never] # default: auto
[--no-llm] # skip the Foundation Models pass
[--title "…"] [--author "…"]
[--verse] # keep line breaks in short-line passages
[--keep-intermediate <dir>] # per-stage JSON + the model transcript
[--progress-window] # a progress bar window (the Quick Action uses it)
[--verbose]
Exit status is 0 on success, 1 on failure, and 2 on success with warnings (for example,
low-confidence OCR pages, or passages the model declined to review). Progress
(page 37/212) goes to stderr, or to a small window with --progress-window.
When the PDF has no title or author, they come from a download-style file name
(Title _ Subtitle -- Last, First_.pdf) or from the title page.
Profiles:
xteink: for the Xteink X4 on CrossPoint-family firmware. It emits a restricted
XHTML subset, a CSS file under 2 KB, chapter files of at most 64 KB, grayscale baseline
JPEGs no wider than 480 px, and a 480×800 cover.generic: for Kindle, Kobo, Apple Books, and KOReader. Limits are relaxed: color
images up to 1200 px wide and no file splitting.PDF ─► Ingest ─► Layout heuristics ─► LLM review ─► Structure ─► EPUB
text layer headers/folios narrow yes/no chapters EPUB 3 + NCX
or OCR columns, verse questions only footnotes own zip writer
paragraphs, joins figures
Sources/PDF2EPUBCore/Ingest): PDFKit's attributed strings report every font
as "Helvetica", so GlyphScanner walks the content stream with CGPDFScanner to recover
each glyph's real font (italic, bold, small caps), size, and position. It then matches them
to PDFKit's characters. A scanner's invisible OCR layer is kept when it carries real
typography (several fonts, italics: professional OCR, which also marks superscript note
numbers) and reads well. A page goes to Vision OCR when it has no text layer, a plain
single-font hidden layer (Internet Archive style), or text that mostly fails the spell
checker.OCR/VisionOCR.swift): RecognizeDocumentsRequest, with RecognizeTextRequest
as a fallback, pinned to the book's language (auto-detection turns old-style numerals into
Cyrillic). Tables are cropped as images. OCR'd text then gets deterministic repairs
(Cleanup/): small caps read as mixed case (SeTTLeMENTS → SETTLEMENTS), Cyrillic
look-alikes, fullwidth punctuation, and glyph mis-mappings learned from the book itself
(one layer's italic "k" came out as "").Layout/): running heads and folios (by repetition, position, and page-number
sequence); two-column reading order; paragraphs (by indent, gaps, and short last lines);
verse (capitalized lines, the fill test, alternating indents); headings; footnotes;
dehyphenation that follows the book's own spelling ("to-day" stays if the book writes
it); and joins across page breaks.Cleanup/): only for the cases the heuristics flag. See below.Structure/, EPUB/): chapters come from the PDF outline; else
from the book's printed Contents page, placed through the printed page numbers (this
works even when chapter titles are display art); else from detected headings. Footnotes
become linked notes in the same file; references to a Notes chapter link to the right
endnote and back. Figures and captions are paired. Packaging uses its own zip writer, with mimetype
stored first.Foundation Models works from a plain CLI binary and from the Quick Action. No app bundle or entitlements are needed.
Guardrails: the default model refuses violent classical passages (the Iliad). The
SystemLanguageModel(guardrails: .permissiveContentTransformations) mode accepts them,
but only for plain String output. Guided @Generable output still refuses, so the
pass asks for short string answers and parses them itself.
Vision: RecognizeDocumentsRequest text is excellent, but its paragraph grouping
is unreliable around dialogue, and its line heights vary by about ±30%. OCR'd pages are
therefore segmented by geometry, and size only counts when the difference is large.
The ~3B model can't follow a multi-block reply format and likes to "help" by modernizing (unnumber'd → unnumbered, ancle → ankle). So every request is one narrow question:
A spelling change is accepted only if it turns a non-word into a word through a typical OCR confusion (rn↔m, tl↔d, c↔e). Digits, elisions, diacritics and ligatures are never removed, and names are left alone. With these rules, the pass is never worse than heuristics alone on the test fixtures. It asks at most 400 questions per book.
swift build -c release
swift test
If xcode-select points at an older Command Line Tools install, prefix commands with
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer. The Scripts do this
automatically.
Scripts/make-fixtures.swift typesets public-domain excerpts from
Tests/PDF2EPUBCoreTests/Fixtures/sources (Pride and Prejudice, Pope's Iliad) into
book-like PDFs. These have running heads, folios, hyphenation, italics, and verse with
turnovers. It also makes scanned and junk-OCR-layer variants, plus the expected text.
The golden tests require exact paragraph-for-paragraph output on the born-digital PDFs,
and a minimum match ratio on the scans.java -jar epubcheck.jar book.epub. Every corpus book currently passes with 0 errors and
0 warnings.swift Scripts/excerpt-pdf.swift book.pdf excerpt.pdf 1-60,296-305 copies
chosen pages (text layer included) into a smaller test PDF. Keep local test books in
TestCorpus/, which git ignores.--keep-intermediate dir writes blocks-heuristic.json, blocks-cleaned.json,
chapters.json, and llm-transcript.txt.--dump-page N [--ocr always] prints one page's extracted lines with geometry and
styles.PDF2EPUB_DEBUG_VERSE=1 prints the verse-detection statistics for each region.Claude Opus 5.5 made all of this. I take no credit beyond the initial desire and some busy work to bundle it up. It's MIT license so I also take no legal responsibility. I found this to be a useful tool. Hopefully this tool will be useful to you.