Webfold
Menu

Format · OCR

Text from scanned PDF pages, through Poppler and Tesseract run on your machine.

When a PDF page has no text layer, Webfold renders it with pdftoppm and recognises it with Tesseract on your own machine: with an interim page rule in a build that was never committed it matched Tesseract's character error rate on a scanned text page (0.000) and a blurred scan (0.023), and it passed 47.2% of olmOCR-bench's scanned tests against 47.1% for Tesseract alone, so it cannot beat Tesseract, and it accepts only PDF input, not PNG or JPEG.

When a PDF has no readable text, or a page has none, or a page is a scan with only a stamp of text on it, the library renders the page with pdftoppm and recognises it with tesseract. The output is Markdown with a marker before each recognised page. Image files such as PNG or JPEG are not accepted; the input must be a PDF path.

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 pdf scan.pdf --ocr-lang eng+deu --ocr-max-pages 50 --ocr-max-dim 3000 --ocr-timeout 300
2webfold pdf scan.pdf --chunk 4000 --jsonl

Input

What input does OCR reading accept?

When OCR runs

Native parsing runs first. If no page has an alphanumeric text line, or native parsing fails, the library OCRs the whole file. Otherwise a per-page rule decides: a page with no text, or a scan whose only text is a stamp (images cover half the page or more and the text is under 0.4 alphanumeric characters per 1000 pt²), is OCRed, and every other page keeps its native text. The webfold command line and the MCP tool use the same rule, and `webfold pdf --no-ocr` turns OCR off. The rule and the flag are in Webfold's main branch, not in the 0.1.0 release.

Input

A PDF file path. The entry points take a path, not bytes, and pass that path to pdftoppm. No code path converts PNG, JPEG or TIFF files; convert_bytes has no arm for image extensions.

Languages

Tesseract language codes in `OcrOptions::languages`, default `eng`. Several languages are joined with `+`, for example `eng+deu`. Only ASCII letters, digits, `_` and `+` are accepted, up to 64 characters. The code does not check that the language data is installed.

Page, size and time limits

Pages: default 20, allowed 1 to 2,000. Longest rendered edge: default 2,400 px, allowed 256 to 4,000. Wall-clock budget: default 120 seconds, allowed 1 to 3,600, shared by rendering and recognition. Workers: 1 to 16 Tesseract processes at once. Pages render in windows of up to 25, sized so that one window's PNG files stay within 512 MiB.

Output shape

Markdown with `<!-- page N -->` before each page that has recognised text. Pages with blank OCR output are dropped. If the page cap is reached while the document continues, the output ends with a notice that later pages may be omitted.

Metrics and log (library)

`OcrConfig` takes an `OcrMetrics` object, which renders Prometheus text, and a JSON-lines log path. The log has `started` and `finished` events and per-stage `flame` events. The webfold CLI sets neither.

Output

What comes out?

Page-marked Markdown

Each recognised page is written after a `<!-- page N -->` marker, in page order. In a mixed file, native pages keep their native text under the same markers, and OCR text fills the empty pages.

Tesseract plain text

The pipeline reads the `.txt` file that Tesseract writes for each page, capped at 4 MiB. No bounding boxes, word confidences or hOCR are kept, so the output has no coordinates.

Truncation notice

When the page limit is reached and the document has more pages, the output ends with `[OCR stopped at the configured limit of N pages; later pages may be omitted]`. The notice is part of the string, so downstream code must check for it.

Document graph (library)

`graph::from_pdf_path` builds a `DocumentGraph` from the extraction output with one region per page, and labels the source as OCR with the model string `superdocuments-0.8.1`, psm 3 and language `eng`, whether or not OCR ran.

JSON-lines log entries (library, when a log path is set)

A `started` event records the input size, languages, page limit, image size and timeout. A `finished` event records the outcome (native, ocr or error), whether OCR was used, whether the run was truncated, pages rendered, bytes rendered, bytes OCR'd, and elapsed, render and recognise times in milliseconds.

How it works

What does the code do, step by step?

  1. 1

    Read and parse natively

    The PDF is read up to the 256 MiB cap and the native parser runs. If it fails, or no page has text, the whole file goes to OCR. Otherwise each page is judged by the page rule, and only the pages it flags go to OCR.

  2. 2

    Choose the pages

    The selected pages are the whole file, or only the pages the page rule flags in a file that has some text. Both are limited to the first `max_pages` pages, and a window is rendered only when it contains a selected page.

  3. 3

    Render a window

    pdftoppm runs as `pdftoppm -f FIRST -l LAST -scale-to PX -png INPUT PREFIX` for up to 25 pages at a time, under the shared deadline. The code does not set a DPI flag. The PNG files of one window must total no more than 512 MiB.

  4. 4

    Recognise each page

    tesseract runs as `tesseract IMAGE BASE -l LANGS --psm 3`, with `workers` processes at a time. When there is more than one worker, each process gets `OMP_THREAD_LIMIT=1`. Results are collected in page order.

  5. 5

    Collect and cap the text

    The `.txt` output of each page is read with a 4 MiB limit, trimmed, and dropped if blank. If the collected text passes 16 MiB across the request, the run fails with an error.

  6. 6

    Assemble and clean up

    Pages are joined with `<!-- page N -->` markers, and the truncation notice is appended when the cap was hit with document remaining. Rendered PNG files are deleted after each window, and the temporary directory is removed when the run ends.

Measured

What did we measure, and how?

The files that go to OCR: accuracy and time against Tesseract run directly, and two public benchmarks

Run of 2026-11-13

On the pages where the route runs pdftoppm and Tesseract, how accurate is the text, and how does it compare with running Tesseract yourself?

Corpus. Three of the 8 PDFs in tests/fixtures of the superdocuments repository (Apache-2.0) that need OCR: a scan, a blurred scan and a file that mixes a text page and a scanned page; each has a hand-checked .expected.txt. Also the scanned categories of a seeded 144-PDF sample of olmOCR-bench (ODC-BY) and the Noisy OCR Dataset (CC BY 4.0, 12 pages in 3 noise conditions).

ToolVersionRole
Webfold PDF routesuperdocuments branch ccr-11c60241-pdf (the PDF parser changes), plus an interim OCR page rule in an uncommitted build (OCR a page with an image and under 100 letters or digits). No commit hash is recorded for itSubject. This is the library route, not a rebuilt webfold pdf binary. Webfold main pins superdocuments 68c216f, the merge of #37, which carries a different rule (image coverage and text density). The 0.1.0 release predates it. The numbers were not re-measured on that binary.
Tesseract directtesseract 5.3.4 with Poppler 24.02.0 pdftoppm at 300 dpi, page 1 only, default page segmentationBaseline: the same engine without the library around it.
liteparse OCR@llamaindex/liteparse 2.18.0, with OCRAnother OCR route, on the public benchmarks.
MetricHow it is computed
Character error rate (CER)Levenshtein distance between the alphanumeric, lower-cased output and the expected text, divided by the length of the expected text, with HTML tags removed first. Lower is better.
olmOCR-bench pass rateShare of tests passed on the scanned categories (old scans, scans with math, tiny text) and their baseline tests, as the unweighted mean of the per-category pass rates.
TestRouteRoute timeTesseract aloneTesseract time
Scanned text, 35.1 KB0.000640 ms0.0001,759 ms
Scanned, blurred, 71.6 KB0.023776 ms0.0231,915 ms
Mixed native and scanned, 27.9 KB0.1191,005 ms0.212 (page 1 only)1,855 ms
olmOCR-bench, scanned sets, share of tests passed47.2%not timed47.1% (liteparse OCR 35.5%)not timed
Noisy OCR Dataset, mean character error rate0.068not timed0.066 (liteparse OCR 0.307)not timed
1bash tests/pdf-extractors-v1/reproduce.sh [--quick] [WORKDIR]

Reproduce: webfold-bench repository, tests/pdf-extractors-v1/README.md. Figures read from webfold-bench repository, tests/pdf-extractors-v1/results/SUMMARY.md and results/website-entry.json.

Limits of this measurement

  • Three small fixtures; one of them (the mixed page) is a scan inside a file that also has text. Character error rate over short texts moves a lot on one wrong letter: the blurred scan's 0.023 is one error in 43 characters of expected text.
  • The route calls Tesseract, so it cannot be more accurate than Tesseract on a page. Tesseract direct was run on page 1 only, so it cannot read the scanned second page of the mixed file, and its times are not like for like with the route's.
  • The route rows come from an interim OCR page rule in a build that was never committed. The rule that merged looks at image coverage and text density. Webfold main pins superdocuments 68c216f, the merge of that rule, so its command line has the merged rule. The numbers were not re-measured on that binary.
  • The olmOCR-bench and Noisy OCR rows come from a Webfold command-line build whose revision the result files do not record.
  • OCR here is Tesseract at its default page-segmentation mode with no deskew or cleanup. No model-based OCR was run, and liteparse's OCR is the only other engine compared.
  • There is no orientation handling: on the rotated page the native parser read the text layer, and Tesseract direct gave no scorable text.
  • This run replaces the 14 October and 27 September runs, which are withdrawn: their error-rate code counted extra leading text as free.

PDF text and OCR 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
OCR is entered for the whole file when native parsing fails or no page has alphanumeric text. Otherwise a per-page rule picks the pages to OCR.Read from the codesuperdocuments/src/pdf/mod.rs (extract_file_with_ocr_inner); superdocuments/src/pdf/page_profile.rsextract_file_with_ocr_inner parses the pages and their geometry first. If any page has embedded text, page_profile::verdicts flags the pages with no text and the scans whose only text is a stamp, and those pages go to merge_native_and_ocr. A failed native parse, or a file with no text on any page, goes straight to tesseract_ocr_with_observer on the whole file.
In a file that has some text, the native text of each page the rule does not flag is kept, and the flagged pages are OCRed and spliced in page order.Read from the codesuperdocuments/src/pdf/mod.rs (merge_native_and_ocr, page-level routing); superdocuments/src/pdf/page_profile.rsextract_pages_markdown gives per-page text, and merge_native_and_ocr writes <!-- page N --> before each page with text. A page is flagged when it has no alphanumeric native text, or when images cover half the page or more and its text is under 0.4 alphanumeric characters per 1000 pt².
Pages are rendered with pdftoppm using a page range, a longest-edge size and PNG output, and no DPI flag.Read from the codesuperdocuments/src/pdf/mod.rs:1078-1088Arguments: -f first, -l last, -scale-to max_image_dimension, -png, the input path and a temporary prefix. stdout and stderr are discarded (run_bounded, mod.rs:1360-1366).
Pages are recognised with tesseract in PSM 3, with the language list from the options, and with OpenMP limited to one thread when more than one worker runs.Read from the codesuperdocuments/src/pdf/mod.rs:1143-1157Arguments: IMAGE BASE -l LANGUAGES --psm 3. The text is read from BASE.txt. OMP_THREAD_LIMIT=1 is set when workers > 1.
Option ranges are checked before any process starts: language string, page count 1 to 2,000, image size 256 to 4,000, workers 1 to 16, timeout 1 to 3,600 seconds.Read from the codesuperdocuments/src/pdf/mod.rs:713-752OcrOptions::validate returns InvalidInput errors. Its limit constants are at superdocuments/src/pdf/mod.rs:22-29.
Every child process runs under one shared deadline. A child that passes the deadline is killed and reported as timed out.Read from the codesuperdocuments/src/pdf/mod.rs:1360-1384run_bounded polls try_wait every 10 ms and calls kill and wait at the deadline. The deadline is set once per request (mod.rs:1054).
Temporary files live in a per-request directory under the system temp directory, created with mode 0700 on Unix and removed on drop.Read from the codesuperdocuments/src/pdf/mod.rs:1320-1358OcrTempDir::create uses std::env::temp_dir() and a process id plus counter name. Drop calls remove_dir_all.
The webfold CLI sets OCR defaults from environment variables: language eng, 20 pages, 2,400 px, 120 seconds, and one worker. Its help text says the timeout is at most 600 seconds, but the code does not enforce that limit.Read from the codewebfold/src/cli/document.rs:256-277ocr_options_from_env reads WEBFOLD_OCR_LANG, WEBFOLD_OCR_MAX_PAGES, WEBFOLD_OCR_MAX_DIM and WEBFOLD_OCR_TIMEOUT_SECS. The doc comment at line 258 says at most 600. The library accepts up to 3,600 seconds.
The webfold pdf command takes the OCR flags and writes each into its environment variable before extraction. --no-ocr starts neither pdftoppm nor tesseract.Read from the codewebfold/src/main.rs (the pdf arm)The flags --ocr-lang, --ocr-max-pages, --ocr-max-dim and --ocr-timeout map to WEBFOLD_OCR_LANG, WEBFOLD_OCR_MAX_PAGES, WEBFOLD_OCR_MAX_DIM and WEBFOLD_OCR_TIMEOUT_SECS. --no-ocr, before or after the file name, returns the native text and names on stderr the pages it did not read. The MCP tool webfold_pdf (src/bin/webfold_mcp.rs) takes no OCR flags and runs webfold pdf.
The input is a PDF path only. No image-file arm exists in the document converter.Read from the codesuperdocuments/src/document.rs:849-867convert_bytes handles office, ebook, CSV, HTML and text extensions and returns unsupported document extension for anything else, including png, jpg and tif. README.md format matrix says the OCR entry points are file paths only.
The test for the OCR path is ignored by default and needs pdftoppm and tesseract on PATH. It checks a raster-only fixture against its phrase and a Flate and ASCII85 fixture.Covered by a testsuperdocuments/src/pdf/mod.rs:1629 and superdocuments/tests/fixtures/README.mdNamed test: tesseract_fallback_handles_raster_and_unsupported_filter_pdfs. It asserts that scanned-text.pdf recovers the phrase Scanned fixture: OCR works. after normalising case and punctuation. Not run for this sheet.
The mixed-file page cap keeps native text and notes the truncation.Covered by a testsuperdocuments/src/pdf/mod.rs:1599Named test: mixed_fixture_scan_beyond_the_page_cap_keeps_native_text_and_notes_truncation, with max_pages set to 1. It expects the native first page and the limit notice. It is not marked ignored, and it calls pdftoppm. Not run for this sheet.
A child process that passes its deadline is killed and reported as an error.Covered by a testsuperdocuments/src/pdf/mod.rs:1691Named test: ocr_child_process_respects_deadline. It uses sleep and sh, not pdftoppm or tesseract. Not run for this sheet.
Ranges of OCR pages are split into windows with no gaps and no overlaps.Covered by a testsuperdocuments/src/pdf/mod.rs:1536Named test: ocr_windows_cover_the_page_range_without_gaps_or_overlap. It is pure and needs no tools. Not run for this sheet.
On the scanned fixtures the route's character error rate equals Tesseract run directly on page 1: 0.000 on a scan and 0.023 on a blurred scan. On the mixed file it is 0.119 against 0.212.MeasuredPDF text and OCR benchmark; webfold-bench repository, tests/pdf-extractors-v1/results/SUMMARY.mdEight PDFs from superdocuments/tests/fixtures; character error rate against each file's expected text with HTML tags removed first; median of 6 calls after 1 warm-up, on a shared 4-core machine at a load average of 12 to 30; Poppler 24.02.0 and Tesseract 5.3.4. The route calls Tesseract, so it cannot be more accurate than it. Tesseract direct reads page 1 only, so it cannot read the scanned second page of the mixed file. The Webfold rows come from a build with an interim OCR page rule that was never committed, not the rule that merged. The results record no commit hash for that build. On the scanned sets of olmOCR-bench the route passes 47.2% of tests and Tesseract alone 47.1%.
A page that goes to OCR takes 0.6 to 1.0 seconds on these fixtures, against 19 to 26 ms for a pdftotext process that cannot read the scans.MeasuredPDF text and OCR benchmark; webfold-bench repository, tests/pdf-extractors-v1/results/SUMMARY.mdEight PDFs from superdocuments/tests/fixtures; character error rate against each file's expected text with HTML tags removed first; median of 6 calls after 1 warm-up, on a shared 4-core machine at a load average of 12 to 30; Poppler 24.02.0 and Tesseract 5.3.4. The route took 640, 776 and 1,005 ms on the three files it OCRed. Tesseract direct took 1,759 to 1,915 ms at 300 dpi on page 1, which is not like for like because the route scales pages to a longest edge of 2,400 px by default.
A blurred scan scores 0.023 through OCR against 0.628 for pdftotext and MarkItDown, which recover only the native title.MeasuredPDF text and OCR benchmark; webfold-bench repository, tests/pdf-extractors-v1/results/SUMMARY.mdEight PDFs from superdocuments/tests/fixtures; character error rate against each file's expected text with HTML tags removed first; median of 6 calls after 1 warm-up, on a shared 4-core machine at a load average of 12 to 30; Poppler 24.02.0 and Tesseract 5.3.4. One fixture; 0.023 is one error in 43 characters of expected text.

What are the limits?

Know the limits

  • External tools are required and not probed. pdftoppm (Poppler) and tesseract must be on PATH, and the Tesseract language data for each requested language must be installed. The code does not check either at start-up. A missing binary is a spawn error, which the webfold CLI prints as pdf error reading and exits with status 1.
  • Recognition quality is measured on eight small test PDFs and on the scanned sets of two public benchmarks. Three of the eight fixtures go to OCR, with character error rates of 0.000, 0.023 and 0.119, equal to Tesseract run directly on one-page scans. On olmOCR-bench's scanned sets the route passes 47.2% of tests, Tesseract alone 47.1% and liteparse's OCR 35.5%. There is no orientation detection, deskew or image cleanup: the only Tesseract option is --psm 3. Treat OCR text as a first pass.
  • Resolution is set by page size, not by DPI. Pages are scaled so the longest edge equals the image dimension (2,400 px by default, up to 4,000). Effective resolution therefore depends on the page's physical size, and no DPI is passed to pdftoppm.
  • Page and time caps apply by default. The default page cap is 20 in both the library and the CLI, and a longer document is cut with a notice. The default time budget is 120 seconds for rendering and recognition together. The CLI help text says at most 600 seconds, but that is not enforced, and the library accepts up to 3,600.
  • Only PDF input is supported. PNG, JPEG and TIFF files are not converted, because every OCR path starts from pdftoppm on a PDF path, and the converter has no image arm.
  • The output is plain text with no layout. Tesseract's text file is the only result kept, so bounding boxes, confidences, table cells and reading-order repair are not available. Columns and tables are recognised only as far as Tesseract's own segmentation reads them.
  • The MCP tool cannot set OCR options. The MCP tool webfold_pdf takes only path, chunk_chars and timeout_seconds (its own run timeout, 1 to 600 seconds, default 120), so OCR options come from WEBFOLD_OCR_* environment variables and a long OCR run can exceed the MCP timeout. The OCR rows of the benchmark come from a build with an interim page rule that was never committed, not the rule that merged.
  • Deployment needs a writable temporary directory and enough disk for a render window of up to 512 MiB, plus one Tesseract process per worker, up to 16. Nothing in the PDF or OCR code makes a network call, so the pages stay on the machine that runs the command.

Compared

How does it compare with other tools?

Compared withWhat the evidence saysSource
PyMuPDFPyMuPDF offers OCR through Tesseract (via MuPDF) or RapidOCR, and its README recommends RapidOCR. Our OCR calls only Tesseract, through an external process.PyMuPDF README
pdfminer.sixpdfminer.six's README documents no OCR engine; hOCR appears there as an output format, not a recogniser.pdfminer.six README
marker, MinerU and doclingThese run model-based OCR: marker's README describes vision-model OCR, MinerU runs OCR in its basic, standard and advanced tiers, and Docling has OCR on by default with several engines. They use model weights: Docling's documentation says models download on first use, and MinerU's README lists model downloads of about 0.8 to 3 GB. Our OCR uses no model, but it needs Tesseract and pdftoppm installed locally.marker README; MinerU README; Docling installation
Image file to textPyMuPDF's README input table lists image files (PNG, JPEG, BMP, TIFF, GIF) and it can OCR them. Webfold does not read image files: the code takes PDF paths only.PyMuPDF README

Where should I read next?

FAQ

Frequently asked questions

Does webfold OCR every PDF?

No. Native text is read first. OCR runs for the whole file when it has no readable text or native parsing fails. Otherwise it runs for the pages the page rule flags: pages with no text, and scans whose only text is a stamp. webfold pdf --no-ocr turns it off.

Which languages can it read?

Any language whose Tesseract data is installed. Pass one code, or several joined with +, such as eng+deu. The default is eng. The code checks only the syntax of the list, not whether the data is installed.

Why are some pages missing from the output?

Two causes are visible in the code. The page was past the page cap, in which case the output ends with a notice. Or a tool failed, which returns an error rather than partial output. With --no-ocr, pages the rule would have OCRed are named on stderr and left out.

Can I pass a PNG or JPEG?

No. The OCR entry points take a PDF path and render it with pdftoppm. Image files are not converted by any path in this code.

Is OCR private?

The OCR code runs pdftoppm and tesseract on the local machine and makes no network calls. Temporary page images are written to the system temporary directory and deleted after each window. Metrics and logs are written only when a library caller sets them up.

How accurate and how fast is the OCR?

On three small scanned test PDFs the character error rate was 0.000, 0.023 and 0.119, equal to Tesseract run directly on the one-page scans, and each file took 0.6 to 1.0 seconds. On the scanned sets of olmOCR-bench the route passed 47.2% of tests and Tesseract alone 47.1%. The measured section above has the protocol and the limits. Nothing here compares Tesseract with other OCR engines, apart from liteparse's OCR at 35.5% on olmOCR-bench.

Read OCR with Webfold.

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