Webfold
Menu

Format · EPUB

EPUB books to Markdown, with two reading orders depending on the route.

Webfold reads EPUB books to Markdown by two different routes: the library's spine reader follows the OPF reading order, while webfold convert converts every .xhtml or .html entry in file-name order; on 4 EPUB fixtures its hot median is 71.1 µs against 381.2 µs for anydoc, another document converter, and images, styles, fonts and DRM are not handled.

Reads EPUB files, which are ZIP packages of XHTML chapters. The spine reader (epub::read and epub::extract_text) follows the OPF reading order and returns one Markdown string. The convert route used by webfold convert ignores the package structure and converts every .xhtml or .html entry in file-name order, and images, styles, fonts and DRM are not handled.

Last updated , the newest measurement or check on record for this page.

See it work

How do I try it from wherever I work?

1webfold convert book.epub > book.md
2webfold fetch <url-of-epub> --dump text

Input

What input does EPUB reading accept?

EPUB packages (container and OPF)

epub::read needs META-INF/container.xml with a rootfile full-path, then the OPF package it names. It reads manifest items (id, href, media-type), spine itemrefs and the OPF title. The version field is not checked, so EPUB 2 and EPUB 3 packages go through the same parser. It returns an error if the container or the OPF is not UTF-8.

Spine order, including non-linear items

The spine is the list of itemref elements in order. The linear attribute is not read, so items marked linear="no" (often notes, covers or supplementary pages) are included in the walk.

Routes that use the spine

epub::extract_text, and webfold fetch <url> --dump text when the response is an EPUB or an HTML page that links to one (cli/epub_walk.rs:260).

Route that does not use the spine

document::convert_bytes('epub'), which webfold convert book.epub uses. It does not call epub::read. It takes every entry whose name ends in .xhtml or .html and sorts the names as strings.

Size bounds

read() refuses input over 256 MiB, an archive with more than 8,192 entries, or an entry over 64 MiB. extract_text() reads each chapter with a 16 MiB cap and stops with an error if the rendered text passes 64 MiB.

Output

What comes out?

Markdown from the spine walk

One string. Each chapter is converted with html::to_markdown and joined to the next by a blank line, in spine order. The base URL is empty, so relative links are not resolved.

Markdown from the convert route

One string, with the same blank-line join. Each chapter is converted with html::to_blocks and html::render using the base epub:///<entry name>. Chapters with no text are skipped. Entries are in file-name order, so a cover or navigation page appears wherever its name sorts.

Book values

Book holds the manifest (id, href, media-type), the spine (id, href) in order, the OPF path and the title. title() returns the OPF title, spine() returns the resolved spine, and manifest_get looks an item up by id.

Image references

An img element becomes ![alt](src), with src resolved against the base that was passed in. Image bytes are not returned, and CSS and fonts are not read.

Errors instead of partial output

extract_text returns Err with no text when a spine entry is missing, a chapter is over 16 MiB, or the total passes 64 MiB. A broken spine idref is skipped with a message on stderr and the rest of the book is read.

How it works

What does the code do, step by step?

  1. 1

    Container

    Read META-INF/container.xml and take the first rootfile full-path as the OPF path (epub.rs:126; parse_container at :243).

  2. 2

    Package

    Read the OPF and build the manifest keyed by id, the spine idrefs in order, and the title (parse_package at :272).

  3. 3

    Resolve the spine

    Each spine idref is resolved to a path under the OPF directory (join_href at :334). An idref that is not in the manifest is skipped with a stderr message (epub.rs:161).

  4. 4

    Spine walk

    extract_text reads each spine entry, capped at 16 MiB, converts it with html::to_markdown, and appends it in spine order (epub.rs:181-205).

  5. 5

    Convert route

    The container and OPF are skipped. Entries ending in .xhtml or .html are listed, the names are sorted, and each is converted with html::to_blocks and html::render (src/document.rs:651-675).

  6. 6

    fetch --dump text

    The fetch command checks whether the response is an EPUB, or points at one, and uses the spine walk (webfold/src/cli/epub_walk.rs:260-265; webfold/src/main.rs:387-392).

Measured

What did we measure, and how?

EPUB: Webfold against anydoc on 4 public fixtures

Run of 2026-09-24

How fast does Webfold convert these files to Markdown, in process and from a fresh process, against anydoc on the same bytes?

Corpus. 4 public fixtures in EPUB from the anydoc repository (tests/fixtures; MIT licence per its README), out of 45 across ten formats. Fixture hashes: document-fixture-sha256-2026-09-24.txt.

ToolVersionRole
Webfoldgit 614483614e4e (document::convert_bytes, in process)Subject. The readers as of that revision, not the newer ones described on this page.
anydocgit 261fc257d17c (anydoc::to_markdown_bytes, Rust library)Comparison converter, linked into the same binary and given identical bytes.
MetricHow it is computed
Hot medianPer fixture: the median of 7 samples, each the mean time of 20 back-to-back in-process conversions of the same bytes, output consumed so it is not optimised away. The cell is the median of those per-fixture medians over the format's fixtures.
Cold medianPer fixture: the median of 7 timed launches (after 2 discarded) of a fresh runner process that reads the file and converts it once, with a warm operating-system cache. Wall time, process start included. The cell is the median of the per-fixture medians.
Converted without errorThe call returned a result. The run records conversion success and output size only; it has no gold text and no fidelity score.
FormatFixturesConverted without error, Webfold / anydocHot median, WebfoldHot median, anydocHot, anydoc over WebfoldWebfold faster, hotCold median, WebfoldCold median, anydocWebfold faster, cold
EPUB4 (3 hand-made)4 / 471.1 µs381.2 µs5.4 times4 of 43.05 ms3.43 ms3 of 4
1# webfold-bench is private until the public release of Webfold. Run this from a checkout of it.
2git clone https://github.com/firecrawl/anydoc
3cd webfold-bench/bench/document-h2h
4cargo run --release -- --fixtures ../../../anydoc/tests/fixtures --out document-results.json --warmup 2 --samples 7 --iterations 20

Reproduce: webfold-bench/bench/document-h2h/src/main.rs; revisions and hashes: webfold-bench/bench/results/document-rust-cold-hot-2026-09-24.provenance.json. Figures read from webfold-bench/bench/results/document-rust-cold-hot-2026-09-24.json and document-fixture-sha256-2026-09-24.txt.

Limits of this measurement

  • Fixtures are tiny: 0.4 to 25 KB each, and most are hand-made feature probes from the anydoc repository, not real-world documents.
  • Speed on success only. Nothing here says the output is right: there is no gold text and no fidelity score, and a conversion that returns almost nothing still counts as converted.
  • One host (Intel Core i5-1340P, rustc 1.96.1) and one run, 2026-09-24. Hot timings are in process and exclude start-up; cold timings include process start, which dominates them (about 2 to 3 ms for both tools), so the cold gap is small.
  • The run measured the Webfold revision named above. The harness manifest pins a later Webfold revision (391c3e290b19) through git dependencies, so a rerun at the pin measures newer readers; to reproduce these figures, use the revisions listed in the provenance file. The readers written since the measured revision have not been benchmarked.

Document conversion benchmark →

Evidence

How do we know each claim?

Each row says how we know. Read from the code means the behaviour is in the implementation at the path shown. Covered by a test names a test in the repository; this page does not claim it was run for this release. Measured points to a benchmark with its protocol and limits.

ClaimHow we knowWhereDetail
epub::read parses the container, the OPF, the manifest and the spine, and skips broken idrefsRead from the codesuperdocuments/src/epub.rs:126read() runs parse_container, parse_package, then resolves each spine idref against the manifest. A missing idref is logged with eprintln (:161) and skipped.
extract_text walks the spine in order and refuses partial outputRead from the codesuperdocuments/src/epub.rs:181Each spine href is read with a 16 MiB cap, converted with to_markdown(&entry, ""), and appended. Exceeding MAX_TEXT_BYTES returns an error.
The OPF parser reads item, itemref and title elements only; linear is not readRead from the codesuperdocuments/src/epub.rs:272parse_package matches item, itemref and title by local name. A search of superdocuments/src/epub.rs for 'linear' finds nothing.
The convert route does not use the spine: it sorts .xhtml and .html namesRead from the codesuperdocuments/src/document.rs:651epub() lists entries ending in .xhtml or .html, sorts the names, converts each with html::to_blocks (base epub:///name) and html::render, and joins them. It does not call epub::read.
fetch --dump text uses the spine walk for EPUB bodiesRead from the codewebfold/src/cli/epub_walk.rs:260try_epub_walk calls superdocuments::epub::read and extract_text for an EPUB body, and for HTML that points at one.
The fetch command calls the EPUB walk for dump text or markdownRead from the codewebfold/src/main.rs:387If --dump is text or markdown, the body is read and try_epub_walk is tried before the normal HTML path.
EPUB size bounds are 256 MiB, 8,192 entries, 64 MiB per entry, 16 MiB per chapter, 64 MiB of textRead from the codesuperdocuments/src/epub.rs:92MAX_ENTRY, MAX_TOTAL, MAX_ENTRIES, MAX_XHTML_BYTES and MAX_TEXT_BYTES constants.
img elements become !alt with the src resolved against the baseRead from the codesuperdocuments/src/html/extract.rs:425The img arm writes !alt) and skips data: URIs. resolve() returns absolute and empty references unchanged.
The EPUB reader has no panic containment and no encryption handlingRead from the codesuperdocuments/src/epub.rs:1A grep of superdocuments/src/epub.rs and src/document.rs finds no catch_unwind and no reference to encryption.xml.
Named unit test: manifest and spine orderCovered by a testsuperdocuments/src/epub.rs:418read_parses_manifest_and_spine_in_order. Not run.
Named unit test: spine walk orderCovered by a testsuperdocuments/src/epub.rs:439extract_text_walks_spine_in_order, a two-chapter fixture built in the test. Not run.
Named unit test: broken idref is skippedCovered by a testsuperdocuments/src/epub.rs:460spine_with_broken_idref_is_skipped_not_panicked. Not run.
Named unit test: missing spine entry is an errorCovered by a testsuperdocuments/src/epub.rs:490extract_text_refuses_a_missing_spine_entry. Not run.

What are the limits?

Know the limits

  • Two reading orders. webfold convert follows sorted file names (src/document.rs:651). epub::extract_text and fetch --dump text follow the spine (superdocuments/src/epub.rs:181). Where file names do not match the reading order, the two outputs differ.
  • Non-linear spine items are included. The linear attribute is not read (superdocuments/src/epub.rs:272).
  • No DRM or encryption handling. META-INF/encryption.xml is not read and nothing decrypts content. A protected book is read as the bytes stored in its chapter files. No protected file was tested.
  • Media is not returned. Image bytes, CSS and fonts are not read. An image appears only as !alt.
  • No table of contents. Navigation and NCX files are not parsed for labels. The convert route reads a navigation file as ordinary XHTML if its name ends in .xhtml or .html.
  • Hard caps: 256 MiB per EPUB, 8,192 entries, 64 MiB per entry, 16 MiB per chapter and 64 MiB of rendered text. A book over the text cap fails as a whole.
  • No panic containment in the epub or document modules. docs/ARCHITECTURE.md says so (line 68), and a grep finds no catch_unwind in either file.
  • Broken spine references are logged to stderr and skipped, so a caller that checks only the return value does not see them (superdocuments/src/epub.rs:161).

Compared

How does it compare with other tools?

Compared withWhat the evidence saysSource
pandocPandoc's manual lists epub as an input and an output format. Fidelity was scored against Pandoc 3.1.3's own reading of 34 EPUB files: 0.804 for superdocuments before the Pandoc-compatibility work, 0.985 after it, 0.831 for MarkItDown 0.1.9. These scores are for superdocuments main. Part of the work behind them is in the revision Webfold pins and the rest, the Pandoc layout, came after it, so the Webfold command line has not been scored. The files were written by Pandoc or LibreOffice.Pandoc manual
markerMarker's README lists EPUB as an input and writes Markdown, JSON, chunks and HTML.marker README
MinerUMinerU's README lists EPUB among its inputs, and says its flash tier parses non-PDF formats such as Office, HTML and EPUB locally with native parsing.MinerU README
PyMuPDFPyMuPDF's README input table lists EPUB, and its output includes Markdown and text.PyMuPDF README
DoclingDocling's supported-formats page lists EPUB among its inputs.Docling supported formats

Where should I read next?

FAQ

Frequently asked questions

Which reading order does webfold use?

It depends on the route. epub::extract_text and webfold fetch --dump text use the spine. webfold convert and document::convert_bytes use sorted file names. If order matters, use the spine route.

Does the reader handle DRM?

No. The source has no encryption code and does not read encryption.xml. No protected file was tested.

Are images and covers extracted?

Yes, through document::extract_images: every image in the manifest comes back with its type, dimensions and the chapter that references it, the cover included. In the Markdown, image references still come out as !alt.

Does it read EPUB 2 and EPUB 3 the same way?

The OPF parser does not check the version. It reads item, itemref and title elements and ignores the rest. The repo has no EPUB fixture file, only the inline test package, which is EPUB 3.

Is there an MCP tool for EPUB?

No. The MCP server exposes webfold_pdf for PDF and no document conversion tool. Use the CLI commands above.

Read EPUB with Webfold.

Run the command-line tool today, and join the waitlist for the hosted API.