PDF to Markdown with OCR, Tables and RAG Chunks avatar

PDF to Markdown with OCR, Tables and RAG Chunks

Pricing

Pay per event

Go to Apify Store
PDF to Markdown with OCR, Tables and RAG Chunks

PDF to Markdown with OCR, Tables and RAG Chunks

Turn PDF links into Markdown with headings, lists and tables, OCR for scanned pages (English and Arabic built in), a quality report per file and heading-based RAG chunks. No browser. Pay per PDF plus per OCR page.

Pricing

Pay per event

Rating

0.0

(0)

Developer

Mohammed Samaan

Mohammed Samaan

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Give this Actor links to PDF files and get back clean Markdown that keeps headings, lists and tables, a quality report that tells you which files to look at, and ready-made chunks (heading path and approximate token count) for a vector database or an LLM. Pages that are only a picture of text, such as scans, are read with OCR. English and Arabic are built in, and Arabic comes back in the right reading order.

It was built for people who feed PDFs to a retrieval pipeline, a contract or invoice archive, or a research corpus, and who need to know which files converted well and which ones a person should check. Everything runs over plain HTTP inside the Actor: no browser, no proxy, no login.

What you get

  • One dataset row per link with markdown, one entry per page (pagesMarkdown), chunks, title, wordCount and warnings.
  • A quality report on every file: how many pages came from the text layer, how many needed OCR, the OCR confidence of each OCR page, pages that failed a content check, pages turned or straightened before OCR, image regions that were not read, tables found and tables kept as aligned text, whether the OCR language is experimental, and needsReview.
  • needsReview flags the conditions listed in the quality report. It does not guarantee that a file with needsReview: false is correct. It is true when an OCR page has a confidence under 70, when the OCR text of a page fails a content check that does not use the engine's confidence (many tokens that cannot be words, words stuck together, many single characters, many tiny lines), when a page could not be read or was unreliable, when pages were left out because of your spending limit or a billing error, or when OCR was stopped after empty pages. It cannot see an error that produces plausible text, so read the pages that matter.
  • OCR decided page by page. A page is read with OCR only when it has almost no text layer (under 25 characters) and holds an image or drawn shapes, or when its text layer is mostly unreadable characters. A blank page is not sent to OCR and not billed. A 50-page report with 3 scanned pages pays OCR for 3 pages.
  • Turned and skewed scans: each page is checked before OCR. Text that runs sideways or upside down is turned back (90, 180 or 270 degrees) and a scan that is tilted by more than half a degree (up to 7) is straightened. Both are reported per page (orientationCorrected, skewCorrected).
  • Images inside text pages: when an image covers at least a quarter of a page that has almost no text layer inside it (for example a table pasted as a picture) the row says Image region not read and lists the page in quality.imageRegionPages. The picture is not read unless you set enableOcr to always.
  • Layout kept: two-column pages are read left column first, headings are found from section numbers, keywords, bold and font size, bullet and numbered lists, tables as Markdown tables when the columns can be trusted (otherwise a fenced block of aligned text and a warning), footnotes separated, wrapped lines joined into paragraphs, repeated page headers and footers removed.
  • Right-to-left text: Arabic (and other right-to-left scripts) is returned in logical order, first word first. Latin words and numbers inside an Arabic line keep their own order.
  • RAG chunks: one chunk per section with its heading path, page range and approximate token count; long sections are split at paragraph boundaries and long tables by rows with the header repeated.
  • Long scans do not come back empty: if a conversion has to be stopped (no page finished for 3 minutes, or 12 times the median page time when that is longer, or 30 minutes in total) the pages already converted are returned with status: "partial" and a warning. OCR pages delivered are billed; no document fee is charged for a partial file.
  • Share links: Google Drive file links, Google Docs, Slides and Sheets links, and Dropbox links are turned into direct download links. Any other link must point straight at the PDF.

Sample input

{
"urls": ["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"],
"enableOcr": "auto",
"ocrLanguages": ["eng"],
"outputFormat": "all"
}

This is the prefilled input. For a scanned Arabic report with some English terms use "ocrLanguages": ["ara", "eng"].

Sample output

Real output of this Actor (a local run with a stand-in for the platform's billing call) on a 5-page test PDF: pages 1, 2 and 5 have a text layer, pages 3 and 4 are scans. Long text is shortened with ....

{
"schemaVersion": 1,
"url": "https://example.org/mixed.pdf",
"status": "ok",
"pages": 5,
"pagesProcessed": 5,
"title": null,
"wordCount": 3100,
"ocrPageNumbers": [3, 4],
"ocrPagesBilled": 2,
"quality": {
"textLayerPages": 3,
"ocrPages": 2,
"blankPages": 0,
"tablesDetected": 0,
"tablesKeptAsText": 0,
"lowConfidencePages": [],
"sanityFailedPages": [],
"unreliablePages": [],
"orientationCorrected": [],
"skewCorrected": [],
"imageRegionPages": [],
"ocrConfidence": [{ "page": 3, "confidence": 93.9 }, { "page": 4, "confidence": 95.5 }],
"meanOcrConfidence": 94.7,
"experimental": false,
"experimentalLanguages": [],
"needsReview": false,
"warnings": []
},
"pagesMarkdown": [
{ "page": 4, "source": "ocr", "confidence": 95.5, "words": 418, "orientationCorrected": 0, "skewCorrected": 0, "markdown": "NIST AI 100-1 AI RMF 1.0\n\nAI system impact assessment approaches can help AI actors understand potential impac ..." }
],
"chunks": [
{ "index": 11, "headingPath": ["Electronic Filing and Payment"], "text": "## Electronic Filing and Payment\n\nBusinesses can enjoy the benefits of filing and paying their federal taxes e ...", "approxTokens": 759, "pageStart": 3, "pageEnd": 5 }
],
"warnings": [],
"error": null,
"errorKind": null,
"charged": true
}

A link that cannot be converted keeps its place in the dataset and says why. It is not charged:

{
"schemaVersion": 1,
"url": "https://example.org/locked.pdf",
"status": "error",
"pages": null,
"markdown": null,
"quality": null,
"ocrPageNumbers": [],
"ocrPagesBilled": 0,
"warnings": [],
"error": "The PDF is password-protected. Give its password in the password input and run again.",
"errorKind": "password_protected",
"charged": false
}

Input

FieldTypeDefaultWhat it does
urlslist of linksrequiredUp to 100 public links per run, 25 MB each. Duplicates are converted once.
enableOcrauto, always, neverautoauto: OCR only on pages that need it. always: OCR on every page (use it when a text layer is known to be wrong, or to read pictures inside text pages). never: text layers only, scanned pages are reported in warnings.
ocrLanguageslist of codes, at most 3["eng"]Language of the text on scanned pages, for example ["ara", "eng"], ["deu"], ["chi_sim"]. Put the main language first. eng and ara are built in; any other official Tesseract tessdata_fast code is downloaded when needed and its output is marked experimental. An unknown code stops the run with a message naming it.
outputFormatall, markdown, per_page, chunksallWhich of markdown, pagesMarkdown and chunks are filled. The others are null.
pageRangetextempty (all pages)For example 1-5, 8, 12-. Page numbers in the output are the numbers of the original file.
maxPages1 to 500100At most this many of the selected pages per file. The row says so in warnings when pages were left out. Values above 300 are accepted only when you set them, and every row then carries a warning that a long OCR job can reach the 30 minute limit and come back partial.
passwordsecret textnoneOpens password-protected files. The same password is tried on every link. It is never written to the output.
maxChunkTokens100 to 8000800No chunk is longer than this. Tokens are approximate (characters divided by 4).
includePageMarkerstrue / falsetrueAdds <!-- page N --> before each page in the Markdown. Chunks keep their page numbers either way.

Output fields

FieldMeaning
schemaVersionAlways 1 for this layout.
url, finalUrl, sourceThe link you gave, the address after redirects, and the address the file came from (differs from url when a share link was rewritten).
statusok, partial, no_text_layer, no_text_found, error, skipped_charge_limit or skipped_repeated_failures.
pages, pagesProcessedPages in the PDF, and pages converted after pageRange, maxPages and any stop.
titleDocument title if one could be found, else null.
markdown, pagesMarkdown, chunksThe converted text: whole document, one entry per page (page, source = text, ocr, unreliable or blank, confidence, words, orientationCorrected, skewCorrected, markdown), and heading-based chunks (index, headingPath, text, approxTokens, pageStart, pageEnd).
ocrPageNumbers, ocrPagesBilledThe pages that were read by OCR and are billed as OCR pages, and how many of them were actually billed.
qualitytextLayerPages, ocrPages, blankPages, tablesDetected, tablesKeptAsText, lowConfidencePages (OCR confidence under 70, or a failed content check, or an unreliable page), sanityFailedPages (page and reasons), unreliablePages, orientationCorrected, skewCorrected, imageRegionPages, ocrConfidence (per page), meanOcrConfidence, experimental, experimentalLanguages, needsReview, warnings.
warningsAnything skipped or uncertain: pages left out, rotated text ignored, tables kept as text, figure labels left out, image regions not read, content-check failures, partial results, billing_error.
error, errorKindSpecific reason when status is error, else null. Kinds: http_error, refused_address, download_failed, file_too_large, empty_file, not_a_pdf, password_protected, wrong_password, unreadable_document, invalid_page_range, page_range_empty, language_unavailable, ocr_unavailable, ocr_failed, render_unavailable, conversion_timeout, conversion_memory, conversion_crashed, conversion_failed, document_too_deeply_nested, charge_limit, circuit_open, unexpected_failure.
chargedWhether the pdf-converted event was billed for this file.

A run summary (SUMMARY in the key-value store) counts converted, partial, no-text, failed and skipped files and the OCR pages billed.

Pricing

Pay per event, two events:

EventPriceWhen
pdf-converted$0.004each PDF converted successfully (text layer or OCR), whatever its size. Not charged for a partial file.
ocr-page$0.007each page that was actually read with OCR and produced reliable text

Worked examples: a 100-page report with a text layer costs $0.004. A 20-page scan costs 20 x $0.007 + $0.004 = $0.144. A 50-page document with 12 scanned pages costs 12 x $0.007 + $0.004 = $0.088. 1,000 text PDFs cost $4.00.

Not charged: links that fail (HTTP errors, refused addresses, files over 25 MB, wrong passwords, damaged or non-PDF files), PDFs with nothing to read (no_text_layer, no_text_found), blank pages, pages where OCR failed, and pages whose OCR text was unreliable (they are delivered, flagged and not billed).

How billing works: the OCR pages of a file are charged first and the pdf-converted event last. If a charge fails (an error from the platform) the converted content is still delivered in full with a billing_error warning, charged: false for the event that failed, and the run carries on. If you set a maximum cost per run, the Actor stops when it is reached: the remaining links get a row with status: "skipped_charge_limit". If the limit is reached part way through the OCR pages of one file, that row keeps the pages that were paid for, drops the others, lists them in warnings and sets quality.needsReview to true. If pay-per-event pricing is not active for a run, files are still delivered, charged stays false, and the run status says so once.

Use with AI agents / MCP

Send this input:

{ "urls": ["https://example.org/report.pdf"], "enableOcr": "auto", "outputFormat": "chunks", "maxChunkTokens": 800 }

Read these fields from each dataset item:

  1. status must be ok (or partial, which means the end of the file is missing: read warnings). If it is error, read error and errorKind. no_text_layer and no_text_found mean there was nothing to read.
  2. quality.needsReview: when true, tell the user which quality.lowConfidencePages, quality.sanityFailedPages or warnings to check before relying on the text. When false, the checks listed above found nothing; that is not proof the text is right.
  3. chunks[].text with chunks[].headingPath to index or quote a section; chunks[].pageStart and pageEnd give the pages to cite.

Benchmark

Settings: the Actor's default input for everything except the OCR language (ocrLanguages: ["ara"] for the two Arabic files, ["eng"] for the others): page markers on, outputFormat: all, enableOcr: auto, 200 dpi OCR rendering. One run per file on a local Windows machine with the same code as the Linux image. "Raw" scores the Markdown exactly as delivered, so every <!-- page N --> marker line counts as extra words; "stripped" removes those marker lines from our output before scoring (the markers are not document text). The scans are clean 200 dpi renders, which are easier than photographed pages, and the files are a small set (8), so treat the numbers as a measurement of these files only.

FileWord F1 raw / strippedHeadings found (extra headings)Other metric
two-column paper0.987 / 0.98931 of 31 (1)4 of 4 reading-order snippets, in order
table-heavy paper0.991 / 0.99123 of 23 (10)12 of 12 rows of one table, every cell right
48-page report0.982 / 0.98030 of 30 (7)
7-page table document0.987 / 0.9891 of 1 (21)
3-page scan (English)0.996 / 0.998character error rate 0.006 raw / 0.001 stripped
Arabic text PDF0.987 / 1.0004 of 4 (0)0 reversed words, logical order
Arabic scan0.983 / 0.9914 of 4 (0)character error rate 0.035 raw / 0.014 stripped, 0 reversed words
mixed text and scan0.995 / 0.9970.999 of the words on the scanned pages, 0.999 on the text pages

Honest notes: an extra heading is a Markdown heading that is not in the file's outline; many of them are real headings the outline does not list, so extra-heading counts are an upper bound. In the same benchmark another extractor scored a higher word F1 on the 7-page table document (1.0 against our 0.987 raw), and the character error rate on the Arabic scan is better than the best other extractor only after the marker lines are stripped (0.014 against 0.023); raw it is worse (0.035). No quality claim is made for those two cases beyond the numbers above. needsReview was false on all 8 files.

Limits

  • OCR quality depends on the page. The built-in models are Tesseract's fast LSTM models. Clean scans at 150 dpi or more read well; photographed pages, low contrast, handwriting, curved text and very small print read worse. The checks above catch some of these (low confidence, scrambled or merged words, sideways text, skew) and miss others: OCR can return plausible but wrong words, and a page can pass every check and still contain errors.
  • Orientation and skew: a page whose text runs sideways or upside down is detected from the pixels, turned and read again; a page that still reads badly is tried at the other orientations (at most 4 readings per page, and the search is switched off for the rest of a file after 3 pages where it found nothing). Skew is measured up to 7 degrees; a stronger tilt, curved or folded pages and mixed orientations on one page are not handled.
  • Image regions inside text pages (a table pasted as a picture, a chart with numbers) are reported but not read unless enableOcr is always.
  • Languages: eng and ara ship with the Actor and were benchmarked. Every other language file (1 to 12 MB, official Tesseract tessdata_fast) is downloaded at the start of the run and its output is experimental: it was not benchmarked, quality.experimental is true and the row carries a warning. This includes Hebrew and Persian, where numbers and line breaks can come out wrong. Up to 3 languages at once; more languages make OCR slower.
  • OCR speed and memory. The platform gives one CPU core per 4 GB of memory, and OCR is CPU-bound, so the default memory is 4 GB (one core). Measured in a Linux container with Node 20 limited to one core, a dense page takes about 4 to 6 seconds; at half a core (2 GB, the minimum) it takes about 8 to 12 seconds. Files are converted one at a time per core. A document is stopped if no page finishes for 3 minutes, or for 12 times the median time per page when that is longer (so a slow CPU is not mistaken for a hang), and in any case after 30 minutes; the pages converted so far are returned as partial. For scans of more than about 200 pages use pageRange to convert them in parts.
  • Empty pages: OCR for a file stops after 5 pages in a row that give no readable text (blank, noise or unreadable); the rest of the image-only pages are not read and not billed, and the row says so. A scan with 5 or more blank separator pages in a row will be cut short.
  • Text layers: 20 seconds without progress stops a document. A file that hangs or needs more than its memory share (60 percent of the Actor memory shared by the files converted at once, one per CPU core) is stopped and returns what it had finished as partial, or an error row (conversion_timeout or conversion_memory) if no page was done; the other links carry on. If 10 files in a row have to be stopped the run ends early and the rest get status: "skipped_repeated_failures".
  • File size: 25 MB per file, 500 pages per file at most (default 100). A row can hold about 8.5 MB; for a very large result the chunks, then the per-page text, then the end of the Markdown are dropped and a warning says so.
  • Tables: a Markdown table is written only when the columns line up on every row. Otherwise the rows stay as aligned text in a code block and quality.tablesKeptAsText counts them. Merged cells, cells with several wrapped lines, tables drawn as pictures and tables on right-to-left pages are not reliable. Tables on OCR pages work only when their columns are clearly separated.
  • Figures and images are not described. Text inside a picture is read only on pages that go through OCR. Axis labels and diagram boxes in text-layer pages are left out and counted in warnings.
  • Arabic text layers that write the letters yeh, heh and kaf with their Persian or Urdu code points (a font quirk) are folded to the Arabic letters when the document contains no letters used only in Persian or Urdu. A real Persian document is left unchanged.
  • Passwords: one password for the whole run. Files with an owner password only (no password needed to open) convert normally.
  • Share links: Drive and Dropbox links must be shared so that anyone with the link can view them. A Drive file too large to be scanned for viruses answers with a confirmation page instead of the file; that returns not_a_pdf.
  • Public links only. The Actor does not log in, use cookies or fetch addresses on private networks. Only ports 80 and 443 are used.
  • Not supported: forms and annotations, embedded files, tagged-PDF structure, handwriting.

FAQ

Does it read scanned PDFs? Yes. Pages without a usable text layer are read with OCR (enableOcr: "auto"). Each page read by OCR is billed once at the OCR page price.

How do I know if the OCR can be trusted? Read quality.ocrConfidence (per page, 0 to 100), quality.lowConfidencePages, quality.sanityFailedPages and quality.needsReview. They flag the conditions described above. A false is not a guarantee.

Which languages? English and Arabic are built in and benchmarked. Set ocrLanguages to another official Tesseract code (for example fra, deu, spa, rus, chi_sim, hin) and its language file is downloaded for the run; the result is marked experimental.

Is the Arabic in the right order? Text layers and scans of the Arabic files in the benchmark come back in logical order, so a search for the first word of a sentence finds the start of the sentence. Check status and quality on your own files.

Can I choose pages? Yes: pageRange such as 1-5, 8, 12-, and maxPages as a cap.

What are the tokens? An estimate of characters divided by 4, for sizing chunks, not for exact model billing.

Why was a page not OCRed although it looks like a scan? The page has a text layer with enough readable text (an already-searchable scan). The text layer is used. Set enableOcr to always to force OCR.

What does partial mean? The conversion had to be stopped (no page finished within the idle budget of 3 minutes or 12 median page times, or 30 minutes passed, or the memory limit was hit). The pages converted so far are in the row and the warning says how many; convert the rest with pageRange.

Open-source credits

Licences were read from each package's and font's LICENSE file in the installed version, and the production tree of the Linux image was scanned with tools/licence-scan.mjs.

Package or fileVersionLicenceUsed for
pdfjs-dist4.10.38Apache-2.0Reading text and positions from PDF; drawing pages for OCR
Liberation Sans fonts (shipped in pdfjs-dist standard_fonts)as in pdfjs-dist 4.10.38SIL Open Font License 1.1 (LICENSE_LIBERATION)Drawing PDF pages that use the standard fonts without embedding them
Foxit standard fonts (shipped in pdfjs-dist standard_fonts)as in pdfjs-dist 4.10.38BSD-style licence of the PDFium authors (LICENSE_FOXIT)The same
tesseract.js7.0.0Apache-2.0OCR engine for Node
tesseract.js-core7.0.0Apache-2.0Tesseract compiled to WebAssembly
tessdata_fast (eng, ara, others on demand)main, 21 Oct 2026Apache-2.0 (LICENSE of the repository)OCR language models
@napi-rs/canvas and its Linux binary0.1.65MIT. The prebuilt binary is built on Google Skia (BSD-3-Clause upstream); the Linux binary package ships no licence or notice file, so the Skia notice is not included hereDrawing PDF pages to images
apify3.7.2Apache-2.0Actor runtime
bmp-js, is-url, node-fetch, regenerator-runtime, zlibjs0.1.0, 1.2.4, 2.7.0, 0.13.11, 0.3.1MITDependencies of tesseract.js
idb-keyval, wasm-feature-detect6.3.0, 1.9.0Apache-2.0Dependencies of tesseract.js

Scan of the shipped Linux image (npm install --omit=dev): 203 entries in the dependency tree, 199 distinct packages (name and version). Of those 199: 141 MIT, 23 Apache-2.0, 15 BSD-2-Clause, 8 ISC, 5 BSD-3-Clause, 2 (MIT OR CC0-1.0), 0BSD, BlueOak-1.0.0 (sax), CC-BY-4.0 (caniuse-lite browser data inside the Apify SDK; attribution given here) and 2 with no licence field in their metadata (map-stream, pause-stream, inside the Apify SDK), which ship licence files (an MIT licence text, and a dual MIT and Apache-2.0 notice). Ten packages declare a permissive licence but ship no licence file: the Skia binary above, tr46 (MIT, under node-fetch) and eight inside the Apify SDK tree. The 16 packages under the PDF, canvas and OCR libraries are 10 MIT, 5 Apache-2.0 and 1 BSD-2-Clause. No GPL, AGPL or OpenRAIL component is installed.