# PDF Text Extractor: Pages, Tables, Markdown & Metadata (`ctriolab/pdf-text-extractor`) Actor

Extract text from PDF URLs, uploads or dataset links: one row per document or per page, page ranges, table-friendly layout order, Markdown and metadata. Password-protected and scanned PDFs are detected and never charged. No OCR, no proxy.

- **URL**: https://apify.com/ctriolab/pdf-text-extractor.md
- **Developed by:** [Ctrio Lab](https://apify.com/ctriolab) (community)
- **Categories:** Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 result rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Text Extractor: Pages, Tables, Markdown & Metadata

Turn PDF links (or an uploaded PDF, or PDF links from another Actor's dataset) into clean text you can search, analyze or feed to an LLM. Choose **one row per document** or **one row per page**, pick page ranges, keep **table rows in the right order**, get a **Markdown** version and the PDF **metadata**. Password-protected and scanned (image-only) PDFs are detected and reported clearly, and you are **never charged for files that fail or have no text**.

No browser, no proxy, no login. Runs on 512 MB.

### What you get

- **Document or page rows.** Document mode puts the full text of each PDF in one row (cheapest) and still lists every page with its character count. Page mode writes one row per page, ready for RAG chunks, search indexes and citations ("page 14 says...").
- **Page ranges** like `1-3, 7, 10-` (10 to the end). Pages outside the file are ignored.
- **Two text orders.**
  - `reading` (default): the order stored in the PDF. Keeps two-column scientific papers in column order.
  - `layout`: rebuilds rows by position, top to bottom and left to right, and separates table cells with a tab. Use it for **tables, invoices, bank statements, forms**, where many PDFs store a table column by column and other extractors return all of column 1, then all of column 2.
- **Markdown** (optional): headings from larger fonts, bullet lists, wrapped lines joined into paragraphs (hyphenated words re-joined), and in layout mode tab rows become Markdown tables.
- **Metadata**: title, author, subject, keywords, creator, producer, creation and modification date (ISO 8601), PDF version, file size, page count.
- **Counts**: characters and words per row, `truncated` when you set a maximum length.
- **Clear problem reports** in the run's `SUMMARY` record and log, never as charged rows:
  - `Password protected: this PDF needs a password` (add it in **PDF password** to open it),
  - `Not a PDF (the server sent text/html)` for login pages and HTML viewers,
  - `Download failed: HTTP 404`, timeouts, files above your size limit,
  - **no text layer**: scanned or image-only PDFs. This Actor does not run OCR, so it tells you which files need OCR instead of returning empty rows. In partly scanned files, `textLayer` is `partial` and `pagesWithoutText` lists the pages.
- **Large files**: downloads are streamed and stopped as soon as the size limit is passed; pages are read one at a time. A 142-page, 3.6 MB IRS publication takes about 7 seconds.

### How to use it

1. Paste PDF links into **PDF URLs** (or upload a PDF, or pick a dataset that has a PDF link field).
2. Choose **One row per** (document or page), and optionally **Pages**, **Text order** and **Markdown**.
3. Start the run and download the results as JSON, CSV, Excel or through the API.

Example input:

```json
{
  "urls": [
    "https://www.irs.gov/pub/irs-pdf/fw4.pdf",
    "https://arxiv.org/pdf/1706.03762v7"
  ],
  "outputMode": "page",
  "pages": "1-3",
  "textLayout": "reading",
  "markdown": true
}
```

### Input

| Field | What it does |
|---|---|
| `urls` | Direct PDF links (http/https). Duplicates are skipped. |
| `uploadedFile` | One PDF uploaded in the Console. |
| `datasetId`, `urlField` | Read PDF links from a dataset field (default `url`), e.g. after a scraper in an integration. |
| `outputMode` | `document` (default) or `page`. |
| `pages` | Page selection such as `1-5, 8, 10-`. Empty = all. |
| `textLayout` | `reading` (default) or `layout` (tables and forms). |
| `markdown` | Also return Markdown. |
| `includeText` | Return plain text (default on). Turn off for Markdown only. |
| `maxChars` | Cut text and Markdown per row (0 = no limit). In document mode reading stops at the limit. |
| `password` | Password for protected PDFs (stored encrypted). |
| `maxFileSizeMb` | Skip larger files (default 100 MB). |
| `downloadTimeoutSecs` | Per-file download timeout (default 120). |
| `maxConcurrency` | PDFs processed at the same time (default 3). |

### Output

Document mode row (shortened):

```json
{
  "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf",
  "fileName": "fw4.pdf",
  "pageCount": 5,
  "pagesExtracted": 5,
  "textLayer": "full",
  "pagesWithoutText": [],
  "charCount": 25763,
  "wordCount": 4954,
  "truncated": false,
  "text": "Form W-4\n2026\nEmployee’s Withholding Certificate\n\nDepartment ...",
  "pages": [{ "page": 1, "charCount": 4747, "hasText": true }],
  "title": "2026 Form W-4",
  "author": "C:DC:TS:CAR:MP",
  "creationDate": "2025-12-11T13:15:41.000Z",
  "pdfVersion": "1.7",
  "fileSizeBytes": 208845,
  "extractedAt": "2026-10-26T04:20:00.000Z"
}
```

Page mode rows have `page`, `pageCount`, `text`, `markdown`, `charCount`, `wordCount`, `truncated`, `title`, `author`, `fileSizeBytes`. Pages without text are skipped in page mode and counted in `SUMMARY.emptyPagesSkipped`.

The `SUMMARY` record (key-value store) has `documentsOk`, `rowsWritten`, `pagesRead`, `failed` (URL and reason), `noTextLayer` and `emptyPagesSkipped`.

### Pricing

Pay per result row: one PDF in document mode, one page in page mode. Failed downloads, non-PDF links, password-protected files without the password and scanned files without a text layer are **not charged**. Higher Apify plans get a discount automatically. Tip: document mode is the cheapest way to get full text; use page mode when you need page numbers per row.

### FAQ

**Does it do OCR?** No. It reads the text layer that most PDFs created by software have. Scanned PDFs are reported as "no text layer" so you can send only those to an OCR tool.

**Which order should I use for tables?** `layout`. It keeps each table row on one line with cells separated by a tab, and in Markdown mode makes a Markdown table.

**Can it open password-protected PDFs?** Yes, if you give the password. Without it the file is reported, not charged.

**Can I use it from an AI agent or my code?** Yes. Call it through the Apify API, the Apify MCP server or any Apify integration, and read the dataset.

**Links that need a login or open an HTML viewer?** Use a direct link to the PDF file. Pages that return HTML are reported as "Not a PDF".

Text extraction uses Mozilla pdf.js (Apache-2.0) through unpdf (MIT).

# Actor input Schema

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

Direct links to PDF files (http or https). Duplicates are skipped. A URL that is not a PDF, returns an error or times out is reported in the run SUMMARY and is not charged.

## `uploadedFile` (type: `string`):

Upload one PDF from your computer. It is processed together with the URLs above.

## `datasetId` (type: `string`):

An Apify dataset (for example from a scraper run) that has PDF links. In an integration after another Actor use {{resource.defaultDatasetId}}.

## `urlField` (type: `string`):

Name of the field that holds the PDF link in each dataset item.

## `outputMode` (type: `string`):

Each result row is one charged item. Document rows also list every page with its character count, so you can still see which pages are empty. Page mode skips pages without text (not charged).

## `pages` (type: `string`):

Optional page selection, e.g. 1-3, 7, 10- (10 to the end). Empty: all pages.

## `textLayout` (type: `string`):

Reading order follows the PDF and keeps two-column papers in column order. Layout order sorts text top to bottom and left to right, so table rows stay on one line even when the PDF stores the table column by column.

## `markdown` (type: `boolean`):

Also return a Markdown-like version: headings from larger fonts, bullet lists, wrapped lines joined into paragraphs, tab rows (layout mode) as Markdown tables. Handy for LLM and RAG input.

## `includeText` (type: `boolean`):

Turn off to get only Markdown (with Markdown on) and smaller rows.

## `maxChars` (type: `integer`):

Cut text and Markdown at this length (0 = no limit). In document mode reading stops once the limit is reached, which saves time on long files. Rows that were cut have truncated: true.

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

Only for password-protected PDFs. Without it such PDFs are reported as "password protected" and not charged.

## `maxFileSizeMb` (type: `integer`):

Larger files are skipped (reported, not charged). The download is streamed and stopped as soon as the limit is passed.

## `downloadTimeoutSecs` (type: `integer`):

Per file.

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

How many PDFs are downloaded and read in parallel. Use more memory for higher values with large files.

## Actor input object example

```json
{
  "urls": [
    "https://www.irs.gov/pub/irs-pdf/fw4.pdf"
  ],
  "urlField": "url",
  "outputMode": "document",
  "textLayout": "reading",
  "markdown": false,
  "includeText": true,
  "maxChars": 0,
  "maxFileSizeMb": 100,
  "downloadTimeoutSecs": 120,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

One row per PDF (document mode) or per page (page mode).

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

PDFs extracted, rows written, failed URLs with the reason, PDFs without a text layer.

# 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.irs.gov/pub/irs-pdf/fw4.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ctriolab/pdf-text-extractor").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.irs.gov/pub/irs-pdf/fw4.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("ctriolab/pdf-text-extractor").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.irs.gov/pub/irs-pdf/fw4.pdf"
  ]
}' |
apify call ctriolab/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ctriolab/pdf-text-extractor"
        }
    }
}
```

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/nRjBou10anemA4a29/builds/uNv0hjYs1cFVdis2g/openapi.json
