# PDF to Text & Markdown: Column-Aware Reading Order (`aiqlabs/pdf-to-text-markdown`) Actor

Convert PDFs to clean text and Markdown in the order a human reads them. Two-column pages are un-interleaved instead of read row by row, repeated running heads and page numbers are dropped, and words broken across line breaks are rejoined.

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

## Pricing

from $10.00 / 1,000 results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PDF to Text & Markdown: Column-Aware Reading Order

Convert PDFs to clean text and Markdown **in the order a human reads them**.

Most PDF extractors return text in the order the file happens to draw it, or row
by row down the page. On a single-column document that is fine. On a two-column
document it is not: the first line of the left column and the first line of the
right column share a baseline, so reading straight across welds them together
and you get two arguments interleaved sentence by sentence.

The output still *looks* like text, which is why it usually goes unnoticed until
something downstream quietly gives wrong answers.

Measured on a real two-column paper (ResNet, CVPR format, first 12 pages):
**265 of 848 rows held text from both columns at once.**

```
Row-major (what column-blind tools return):
  ... classification top-5 LOC error top-5 localization err method network
  testing on GT CLS network on predicted CLS method val test VGG's [41]
  VGG-16 1-crop 33.1 [41] OverFeat [40] (ILSVRC'13) 30.0 29.9 ...

This Actor:
  ... classification top-5 LOC error testing method network on GT CLS network
  on predicted CLS VGG's [41] VGG-16 1-crop 33.1 [41] RPN ResNet-101 1-crop
  13.3 RPN ResNet-101 dense 11.7 ... Table 13. Localization error ...
```

Two separate tables, kept separate.

### What it does

| | |
|---|---|
| **Reading order** | Finds the gutter between columns by projecting every text item onto the x axis, then reads each column top to bottom. A full-width title, rule or footer acts as a band separator, so a headline comes out before both columns and a footer after both. |
| **Running heads and page numbers** | A line that appears at the top or bottom of at least half the pages is furniture, not content. It is removed, and the row tells you what was removed and from how many pages. |
| **Hyphenated words** | `inter-` on one line plus `national` on the next becomes `international`. In justified text this happens dozens of times per document, and every one of them is a token that matches nothing. |
| **Structure** | Headings from the document's own font-size distribution (not a fixed table of sizes), bullets and numbered lists, and paragraphs joined only where the line was actually wrapped. |
| **Markdown or plain text** | Or both side by side, so you can compare. |
| **Per-page output** | Optional, when you need to cite a page number. |

### What it reports about your batch

Every row carries the numbers behind the claim, so you can check it rather than
take it:

- `multiColumnPages` — pages where a gutter was found
- `rowsMergedAcrossColumns` — baselines that hold text from two columns at once
- `shareOfRowsMerged` — the same as a fraction of all rows
- `naiveExtractionWouldScramble` — true when that count is above zero
- `pagesReadAsTables` — pages where a gutter was found but the content said
  "table", so the split was dropped on purpose (see below)
- `boilerplateRemoved` — the exact running heads that were dropped, and from how
  many pages
- `hyphenJoins`, `headings`, `listItems`, `paragraphs`, `bodyFontSize`
- `pagesWithoutTextLayer` — pages that are scans

The run also writes a `SUMMARY` record with the batch totals and the files whose
reading order was worst affected.

### Input

```json
{
  "pdfUrls": ["https://example.com/report.pdf"],
  "outputFormat": "both",
  "detectMultiColumn": true,
  "removeBoilerplate": true,
  "joinHyphens": true,
  "includePageTexts": false,
  "maxPages": 300
}
```

Set `detectMultiColumn` to `false` to see exactly what a column-blind extractor
returns from the same file. That is the honest way to check whether any of this
matters for your documents.

### Where it deliberately does nothing clever

- **It does not run OCR.** Pages with no text layer are counted and reported, not
  read. If a file has no text at all the row comes back as an error saying so.
  For a full pre-OCR audit of a batch — which pages are scans, what the metadata
  leaks, whether the file is encrypted — see
  [PDF Inspector](https://apify.com/aiqlabs/pdf-inspector).
- **One wide table is not two columns.** A table of numbers with a gap down the
  middle projects exactly like a two-column page, but the two want opposite
  treatment: splitting a table tears every row away from its own values. So after
  the split is computed it is checked against the content, and a page that reads
  like cells rather than prose is read row by row instead. The thresholds come
  from measurement: on real pages, prose columns had 84-92% of lines at six words
  or more and 88-93% letters, while a page that was one wide numeric table came
  in at 10% and 49%.
- **A gutter blocked by a figure is not found.** Where a wide figure, equation or
  caption fills the gap, the page falls back to row-major order. This is the
  conservative direction: the page is then no worse than any other extractor's
  output, and the row still says how many pages were affected.
- **No tables are reconstructed.** Table text is returned as text. If you need
  cells as arrays, use a table extractor.
- **It does not render or execute anything.** No headless browser, no canvas, no
  JavaScript from inside the documents.

### Cost

Free. One HTTP request per file, and the parsing is plain CPU work — no browser,
no proxy, no external API. A 12-page paper costs a fraction of a compute unit.

### Verification

- 30 unit tests over the geometry: baseline grouping, inferred spaces, gutter
  detection, the table guard, running-head detection, hyphen joins, paragraph
  joining, heading levels.
- 27 live checks against real PDFs on the open web, including a two-column paper,
  a single-column paper (which must **not** be split), a government form, a
  one-line PDF and an image-only PDF.

Both suites ship in the source. `npm test` and `npm run test:live`.

***

Built by Ai-Q Labs. If it gets something wrong on one of your documents, the
fields above are meant to make that visible rather than hide it.

# Actor input Schema

## `pdfUrls` (type: `array`):

Direct links to PDF files, one per line. Redirects are followed and the URL that actually answered is reported back.

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

Markdown keeps headings and lists. Text is one paragraph per block with no markup. Both returns the two side by side so you can compare them.

## `detectMultiColumn` (type: `boolean`):

Look for the gutter between columns and read each column top to bottom instead of reading straight across the page. Turn it off only to reproduce what a column-blind extractor returns.

## `removeBoilerplate` (type: `boolean`):

A line that appears at the top or bottom of at least half the pages is furniture, not content. Removing it keeps page numbers out of every paragraph you later index or feed to a model.

## `joinHyphens` (type: `boolean`):

Turns "inter-" plus "national" back into one word. Without this, justified text produces tokens that match nothing.

## `includePageTexts` (type: `boolean`):

Adds a per-page array alongside the whole-document output. Useful when you need to cite a page number; it makes rows much larger, so it is off by default.

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

Reading stops after this many pages. The reported page count is still the real one, and the row says the budget was reached.

## `maxOutputChars` (type: `integer`):

Upper bound on the text and Markdown kept per file. Anything beyond it is cut and the row says so.

## `maxFileMb` (type: `integer`):

Downloading stops at this size and the file is reported as too large instead of taking the run down.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each PDF before giving up on it.

## `maxConcurrency` (type: `integer`):

How many PDFs to download and parse at the same time.

## `maxPdfs` (type: `integer`):

Safety limit on how many URLs one run will accept.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "outputFormat": "both",
  "detectMultiColumn": true,
  "removeBoilerplate": true,
  "joinHyphens": true,
  "includePageTexts": false,
  "maxPages": 300,
  "maxOutputChars": 400000,
  "maxFileMb": 50,
  "requestTimeoutSecs": 30,
  "maxConcurrency": 5,
  "maxPdfs": 5000
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `csv` (type: `string`):

No description

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

No description

# 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 = {
    "pdfUrls": [
        "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("aiqlabs/pdf-to-text-markdown").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 = { "pdfUrls": [
        "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    ] }

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

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

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

```

## CLI example

```bash
echo '{
  "pdfUrls": [
    "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}' |
apify call aiqlabs/pdf-to-text-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=aiqlabs/pdf-to-text-markdown",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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