# PDF to Markdown with OCR, Tables and RAG Chunks (`leadharbor/pdf-to-markdown-ocr`) Actor

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.

- **URL**: https://apify.com/leadharbor/pdf-to-markdown-ocr.md
- **Developed by:** [Mohammed Samaan](https://apify.com/leadharbor) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PDF to Markdown with OCR, Tables and RAG Chunks

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

```json
{
  "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 `...`.

```json
{
  "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:

```json
{
  "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

| Field | Type | Default | What it does |
|---|---|---|---|
| `urls` | list of links | required | Up to 100 public links per run, 25 MB each. Duplicates are converted once. |
| `enableOcr` | `auto`, `always`, `never` | `auto` | `auto`: 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`. |
| `ocrLanguages` | list 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. |
| `outputFormat` | `all`, `markdown`, `per_page`, `chunks` | `all` | Which of `markdown`, `pagesMarkdown` and `chunks` are filled. The others are `null`. |
| `pageRange` | text | empty (all pages) | For example `1-5, 8, 12-`. Page numbers in the output are the numbers of the original file. |
| `maxPages` | 1 to 500 | 100 | At 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`. |
| `password` | secret text | none | Opens password-protected files. The same password is tried on every link. It is never written to the output. |
| `maxChunkTokens` | 100 to 8000 | 800 | No chunk is longer than this. Tokens are approximate (characters divided by 4). |
| `includePageMarkers` | true / false | true | Adds `<!-- page N -->` before each page in the Markdown. Chunks keep their page numbers either way. |

### Output fields

| Field | Meaning |
|---|---|
| `schemaVersion` | Always `1` for this layout. |
| `url`, `finalUrl`, `source` | The link you gave, the address after redirects, and the address the file came from (differs from `url` when a share link was rewritten). |
| `status` | `ok`, `partial`, `no_text_layer`, `no_text_found`, `error`, `skipped_charge_limit` or `skipped_repeated_failures`. |
| `pages`, `pagesProcessed` | Pages in the PDF, and pages converted after `pageRange`, `maxPages` and any stop. |
| `title` | Document title if one could be found, else `null`. |
| `markdown`, `pagesMarkdown`, `chunks` | The 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`, `ocrPagesBilled` | The pages that were read by OCR and are billed as OCR pages, and how many of them were actually billed. |
| `quality` | `textLayerPages`, `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`. |
| `warnings` | Anything 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`, `errorKind` | Specific 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`. |
| `charged` | Whether 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:

| Event | Price | When |
|---|---|---|
| `pdf-converted` | **$0.004** | each PDF converted successfully (text layer or OCR), whatever its size. Not charged for a partial file. |
| `ocr-page` | **$0.007** | each 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:

```json
{ "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.

| File | Word F1 raw / stripped | Headings found (extra headings) | Other metric |
|---|---|---|---|
| two-column paper | 0.987 / 0.989 | 31 of 31 (1) | 4 of 4 reading-order snippets, in order |
| table-heavy paper | 0.991 / 0.991 | 23 of 23 (10) | 12 of 12 rows of one table, every cell right |
| 48-page report | 0.982 / 0.980 | 30 of 30 (7) | |
| 7-page table document | 0.987 / 0.989 | 1 of 1 (21) | |
| 3-page scan (English) | 0.996 / 0.998 | | character error rate 0.006 raw / 0.001 stripped |
| Arabic text PDF | 0.987 / 1.000 | 4 of 4 (0) | 0 reversed words, logical order |
| Arabic scan | 0.983 / 0.991 | 4 of 4 (0) | character error rate 0.035 raw / 0.014 stripped, 0 reversed words |
| mixed text and scan | 0.995 / 0.997 | | 0.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`.

### Related Actors

- [Document to Markdown for RAG](https://apify.com/leadharbor/document-to-markdown): text-layer PDFs plus DOCX, XLSX, HTML, CSV and text files, no OCR.

### 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 file | Version | Licence | Used for |
|---|---|---|---|
| pdfjs-dist | 4.10.38 | Apache-2.0 | Reading text and positions from PDF; drawing pages for OCR |
| Liberation Sans fonts (shipped in pdfjs-dist `standard_fonts`) | as in pdfjs-dist 4.10.38 | SIL 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.38 | BSD-style licence of the PDFium authors (`LICENSE_FOXIT`) | The same |
| tesseract.js | 7.0.0 | Apache-2.0 | OCR engine for Node |
| tesseract.js-core | 7.0.0 | Apache-2.0 | Tesseract compiled to WebAssembly |
| tessdata_fast (eng, ara, others on demand) | main, 21 Oct 2026 | Apache-2.0 (LICENSE of the repository) | OCR language models |
| @napi-rs/canvas and its Linux binary | 0.1.65 | MIT. 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 here | Drawing PDF pages to images |
| apify | 3.7.2 | Apache-2.0 | Actor runtime |
| bmp-js, is-url, node-fetch, regenerator-runtime, zlibjs | 0.1.0, 1.2.4, 2.7.0, 0.13.11, 0.3.1 | MIT | Dependencies of tesseract.js |
| idb-keyval, wasm-feature-detect | 6.3.0, 1.9.0 | Apache-2.0 | Dependencies 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.

# Actor input Schema

## `urls` (type: `array`):

Public links to PDF files (up to 100 per run, 25 MB each). Google Drive and Dropbox share links are turned into direct download links; any other link must point straight at the PDF. Links to private networks, localhost and pages that need a login are refused.

## `enableOcr` (type: `string`):

'auto' reads a page with OCR only when it has no usable text layer (a page made of an image, or a text layer full of unreadable characters). 'always' runs OCR on every page. 'never' reads text layers only. Each page read by OCR is billed as an OCR page ($0.007); pages whose OCR text is unreliable are delivered but not billed.

## `ocrLanguages` (type: `array`):

Language codes of the text on scanned pages, at most 3, for example \["eng"], \["ara", "eng"] or \["deu", "fra"]. English (eng) and Arabic (ara) are built in; every other official Tesseract 'tessdata_fast' language is downloaded when needed. List the main language first. Right-to-left languages are returned in logical reading order. Only English and Arabic were benchmarked: any other language is marked experimental in the quality report.

## `outputFormat` (type: `string`):

'all' returns the Markdown, one entry per page and the chunks. 'markdown' returns only the whole-document Markdown. 'per_page' returns one entry per page with its source (text layer or OCR) and confidence. 'chunks' returns only the heading-based chunks for RAG. Fields you do not ask for are null.

## `pageRange` (type: `string`):

Pages to convert, for example 1-5, 8, 12- (an open end means to the last page). Leave empty for the whole document. Page numbers in the output are the numbers of the original document.

## `maxPages` (type: `integer`):

At most this many of the selected pages are converted per PDF. The row says so in its warnings when pages were left out. Above 300 every row carries a warning that a long OCR job can reach the 30 minute limit and come back as a partial result.

## `password` (type: `string`):

Opens password-protected PDFs. The same password is tried on every link in the run. It is never stored in the output.

## `maxChunkTokens` (type: `integer`):

Longer sections are split at paragraph boundaries so no chunk goes over this size. Tokens are approximate (characters divided by 4).

## `includePageMarkers` (type: `boolean`):

Adds  before each page in the Markdown. Chunks keep their page numbers either way.

## Actor input object example

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "enableOcr": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "outputFormat": "all",
  "pageRange": "",
  "maxPages": 100,
  "maxChunkTokens": 800,
  "includePageMarkers": true
}
```

# Actor output Schema

## `documents` (type: `string`):

Table of PDFs: status, pages, OCR pages, confidence, tables and warnings. Open a row to read the Markdown, pages and chunks.

## `summary` (type: `string`):

Counts of converted, no-text, failed and skipped PDFs and the number of OCR pages billed.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ],
    "ocrLanguages": [
        "eng"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("leadharbor/pdf-to-markdown-ocr").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": ["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"],
    "ocrLanguages": ["eng"],
}

# Run the Actor and wait for it to finish
run = client.actor("leadharbor/pdf-to-markdown-ocr").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "ocrLanguages": [
    "eng"
  ]
}' |
apify call leadharbor/pdf-to-markdown-ocr --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,leadharbor/pdf-to-markdown-ocr"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/MpxDQTaJHvVxrcSta/builds/WAXQ3EjFSvAj7welG/openapi.json
