How do I convert a two-column PDF to EPUB?
Updated:
Use a converter that detects the columns before it joins lines into paragraphs, so the left column is read top to bottom and then the right one, instead of reading each line straight across the page. EpubCC does this by splitting each page along the white gaps between columns before rebuilding the text.
Fixed layout vs. reflowable text


Why two columns go wrong
A naive converter reads a page line by line from left to right, so it glues the first line of the left column to the first line of the right column. The result is sentences that alternate between two unrelated texts: the most common failure on academic papers, journals and the Federal Register.
How EpubCC reads columns
- Each page is cut recursively along the widest white gaps (an XY-cut): a vertical gap that crosses the whole region marks a column boundary.
- A gap only counts as a column when each side has at least 2 lines, so a wide space inside one line, such as "2.1 Title", is not mistaken for a column.
- Images are placed as part of the region they sit in, so they stay in reading order.
- A paragraph that continues from the bottom of one column to the top of the next is joined again when the sentence carries on.
Check the result
Layouts that mix one and two columns on the same page, boxes and side notes are the hard part. The free sample shows your own pages side by side with the EPUB, and the benchmark includes two-column papers and a three-column Federal Register document measured for continuity.
Evidence from the benchmark
| Engine | 8-gram recall |
|---|---|
| EpubCC engine (EPUB output) | 0.767 |
| EpubCC engine (plain text) | 0.765 |
| pdf_oxide · extract_text | 0.726 |
| pdf_oxide · to_plain_text (XY-cut) | 0.658 |
| pdf_oxide · Markdown | 0.653 |
| PDFium · text().all() | 0.746 |
| OpenDataLoader PDF (local mode) | 0.749 |
| pdftotext (Poppler) | 0.743 |
| Document | EpubCC engine (EPUB output) | EpubCC engine (plain text) | pdf_oxide · extract_text | pdf_oxide · to_plain_text (XY-cut) | pdf_oxide · Markdown | PDFium · text().all() | OpenDataLoader PDF (local mode) | pdftotext (Poppler) |
|---|---|---|---|---|---|---|---|---|
| OLMo paper (arXiv 2402.00838) | 0.800 | 0.800 | 0.854 | 0.273 | 0.719 | 0.761 | 0.820 | 0.781 |
| Mistral 7B paper (arXiv 2310.06825) | 0.760 | 0.758 | 0.767 | 0.778 | 0.800 | 0.872 | 0.859 | 0.821 |
| Federal Register document 2026-22443 | 0.859 | 0.853 | 0.334 | 0.035 | 0.339 | 0.859 | 0.801 | 0.835 |
Fix mixed-up columns
Convert up to 5 pages free, no account needed.