# PDF Table Preflight Router (`iam_not_real/pdf-table-preflight-router`) Actor

Deterministic PDF table-routing evidence for native extraction, mixed workflows, OCR-likely cases, no-table cases, and unsupported documents.

- **URL**: https://apify.com/iam\_not\_real/pdf-table-preflight-router.md
- **Developed by:** [SUTHEE KOSITWONGSAKUL](https://apify.com/iam_not_real) (community)
- **Categories:** Developer tools
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 completed pdf preflights

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?

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 Table Preflight Router

A deterministic Apify Actor that inspects **public PDF URLs** and routes each document before a pipeline spends more on table extraction or OCR.

The Actor does **not** perform OCR, table extraction, LLM classification, vision-API calls, or manual document work. It only returns bounded routing evidence.

### Input

```json
{
  "pdfUrls": [
    "https://example.com/report.pdf"
  ],
  "maxPagesPerPdf": 50,
  "maxFileSizeMb": 20
}
```

- `pdfUrls`: required, 1–20 public absolute HTTP/HTTPS URLs.
- `maxPagesPerPdf`: optional integer, default 50, hard maximum 50.
- `maxFileSizeMb`: optional integer, default 20, hard maximum 20 MB.

No credentials, private files, OCR keys, AI keys, or paid API inputs are accepted.

### Routing outcomes

Each inspected PDF receives exactly one route:

- `NATIVE_TABLE_EXTRACTION` — deterministic native text/vector/alignment evidence indicates a table candidate that should not require OCR first.
- `MIXED_NATIVE_AND_OCR` — the same PDF contains both native table-candidate pages and scan-like table-candidate pages.
- `OCR_LIKELY_REQUIRED` — table-like raster structure appears on image-dominant pages with little or no selectable text.
- `NO_TABLE_CANDIDATE` — no deterministic table candidate is detected within the inspected document.
- `UNSUPPORTED_OR_ENCRYPTED` — the PDF cannot be safely inspected because it is malformed, encrypted, or outside configured limits, or fetching/validation failed.

`routeConfidence` is a fixed deterministic heuristic confidence value. It is **not** a calibrated probability or ML score.

### Evidence produced

The default dataset contains one diagnostic row per attempted URL. A completed inspected result includes:

- source/final URL, redirect chain, HTTP status and Content-Type;
- downloaded byte count;
- page count and pages inspected;
- encrypted state;
- selectable-text presence;
- `textCoverage`, defined as the fraction of inspected pages with at least 30 non-whitespace selectable characters;
- table-candidate page count;
- scan-likely page count;
- route, route confidence and stable reason codes;
- per-page text characters, approximate image-area ratio, table-candidate state, modality and geometry evidence;
- applied file/page limits;
- whether the custom `pdf-preflight` PPE event is eligible.

Complete schema-valid example (illustrative output shape; values are not a guarantee for future URLs or runs):

```json
{
  "sourceUrl": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "finalUrl": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "httpStatus": 200,
  "contentType": "application/pdf; qs=0.001",
  "bytes": 13264,
  "pageCount": 1,
  "pagesInspected": 1,
  "encrypted": false,
  "selectableText": false,
  "textCoverage": 0,
  "tableCandidatePageCount": 0,
  "scannedLikelyPageCount": 0,
  "route": "NO_TABLE_CANDIDATE",
  "routeConfidence": 0.9,
  "reasonCodes": [
    "NO_TABLE_CANDIDATE"
  ],
  "pages": [
    {
      "page": 1,
      "textChars": 12,
      "imageAreaRatio": 0,
      "tableCandidate": false,
      "candidateCount": 0,
      "candidateConfidence": 0,
      "candidateModality": null,
      "scannedLikely": false,
      "geometry": {
        "horizontalRuleCount": 0,
        "verticalRuleCount": 0,
        "alignedRowCount": 0,
        "recurringColumnCount": 0,
        "denseNumericRowCount": 0,
        "recurringNumericColumnCount": 0,
        "rasterHorizontalLineCount": 0,
        "rasterVerticalLineCount": 0
      },
      "reasonCodes": []
    }
  ],
  "limitsApplied": {
    "maxPagesPerPdf": 50,
    "maxFileSizeMb": 20
  },
  "processingStatus": "completed",
  "paidEventEligible": true,
  "redirectChain": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}
```

### Deterministic table-candidate heuristics

The MVP intentionally uses several local signals instead of AI:

1. **Selectable text** — PyMuPDF extracts page text and word geometry.
2. **Vector rule structure** — repeated horizontal/vertical PDF drawing lines or rectangles provide ruled-table evidence.
3. **Aligned columns** — repeated x-position clusters across multiple short rows provide borderless-table evidence while rejecting paragraph-like rows with many words.
4. **Dense numeric columns** — wide rows with repeated numeric cell x-positions detect dense borderless statistical tables without relaxing the prose safeguard used by the short-row detector.
5. **Image coverage** — displayed image bounding boxes provide a coarse page-area ratio.
6. **Raster grid structure** — only on image-dominant, low-text pages, the page is rendered to 72-dpi grayscale and deterministic horizontal/vertical dark-pixel projections look for table-like grids. This is image processing, **not OCR**.

The Actor processes PDFs sequentially and rasterizes only scan-like pages. It does not retain full-page rasters after each page is inspected.

### Stable reason codes

Document-level reason codes include, as applicable:

- `FETCH_ERROR`
- `TIMEOUT`
- `HTTP_NON_2XX`
- `INVALID_URL`
- `PRIVATE_ADDRESS`
- `DNS_ERROR`
- `FILE_SIZE_LIMIT_EXCEEDED`
- `PAGE_LIMIT_EXCEEDED`
- `INVALID_PDF_MAGIC`
- `CONTENT_TYPE_MISMATCH`
- `PDF_PARSE_ERROR`
- `PDF_INSPECTION_ERROR`
- `PDF_ENCRYPTED`
- `SELECTABLE_TEXT_PRESENT`
- `IMAGE_DOMINANT_PAGES_PRESENT`
- `NATIVE_TABLE_CANDIDATE`
- `SCANNED_TABLE_CANDIDATE`
- `MIXED_TABLE_MODALITIES`
- `TABLE_GEOMETRY_DETECTED`
- `RASTER_TABLE_STRUCTURE_DETECTED`
- `OCR_NOT_REQUIRED`
- `OCR_LIKELY_REQUIRED`
- `NO_TABLE_CANDIDATE`

Page-level reason codes include:

- `SELECTABLE_TEXT_PAGE`
- `IMAGE_DOMINANT_LOW_TEXT`
- `ALIGNED_COLUMNS`
- `DENSE_NUMERIC_COLUMNS`
- `RULED_OR_DENSE_CELL_STRUCTURE`
- `RASTER_GRID_STRUCTURE`
- `TABLE_CANDIDATE`

### Limits and non-chargeable diagnostics

The custom paid event is emitted only for a PDF that:

1. was fetched successfully;
2. stayed within the configured byte/page limits;
3. was not encrypted;
4. was successfully inspected; and
5. produced one of the four usable routes: native, mixed, OCR-likely, or no-table.

Malformed, encrypted, oversized, over-page-limit, failed-download and other unsupported/error rows remain visible as structured diagnostics but do **not** intentionally trigger the custom `pdf-preflight` event.

The current verified Pay-per-Event composition has **no paid `apify-default-dataset-item` event**, so default-dataset rows do not receive a separate paid dataset-item event under the current configuration.

### Pay per Event

Current verified pricing and billing composition:

- `pdf-preflight` — **USD 0.01 per charge-eligible completed inspected PDF preflight** and the primary custom event.
- `apify-actor-start` — **USD 0.00005**, a platform-managed one-time start event. The Actor source does **not** manually emit this event.
- `apify-default-dataset-item` — **not present as a paid event** in the current verified composition.
- User platform-usage pass-through — **OFF**; users are not separately charged the Actor's platform-usage costs under the current configuration.

The source binds `pdf-preflight` only to charge-eligible completed dataset rows:

```python
await Actor.push_data(row, charged_event_name="pdf-preflight")
```

Unsupported/error diagnostic rows are pushed without the custom paid-result event.

### Memory and cost guard

`.actor/actor.json` fixes:

- minimum memory: 256 MB;
- default memory: 256 MB;
- maximum memory: 256 MB.

The current local max-bound benchmark is recorded in `LOCAL_MAX_BOUND_BENCHMARK.json`. It is local evidence only and does not replace Apify-platform runtime measurements.

### Public-network connection guard

Public URL safety is enforced twice: input/redirect validation rejects obvious non-public destinations, and the HTTP transport also resolves the hostname **inside the TCP connection path**, rejects any non-global DNS answer, and connects to the exact validated IP address rather than asking the operating system to resolve the hostname again. TLS SNI and certificate validation continue to use the original request hostname. This closes the earlier DNS-rebinding/TOCTOU gap for direct outbound connections. Proxy environment variables are disabled for this fetch client so they cannot bypass the destination guard.

Every new TCP connection, including connections created for redirect targets, passes through the same public-only network backend.

### Validation notes

The deterministic routing logic is regression-tested against native-table, scanned/image-only, mixed, no-table, encrypted, and malformed PDF cases. Public regression controls include a U.S. Bureau of Labor Statistics statistical-table PDF and the W3C no-table dummy PDF. These cases are targeted regression controls, **not a statistical estimate of accuracy on arbitrary real-world PDFs**.

### Explicit non-claims

This Actor does not:

- run OCR or return OCR text;
- extract tables, cells, CSV, Markdown, JSON content or invoice fields;
- call an LLM, multimodal model, computer-vision API or paid external API;
- determine semantic table correctness;
- guarantee Camelot/Tabula/pdfplumber extraction success;
- classify arbitrary embedded photos as tables unless deterministic grid evidence is present;
- support password entry for encrypted PDFs;
- accept private cloud-drive files or authenticated URLs;
- provide manual audits or document-processing services.

# Actor input Schema

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

1–20 public absolute HTTP/HTTPS PDF URLs.

## `maxPagesPerPdf` (type: `integer`):

Documents above this page count are reported as over-limit and are not paid-result eligible.

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

Documents above this download size are reported as over-limit and are not paid-result eligible.

## Actor input object example

```json
{
  "maxPagesPerPdf": 50,
  "maxFileSizeMb": 20
}
```

# Actor output Schema

## `results` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("iam_not_real/pdf-table-preflight-router").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("iam_not_real/pdf-table-preflight-router").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 '{}' |
apify call iam_not_real/pdf-table-preflight-router --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,iam_not_real/pdf-table-preflight-router"
        }
    }
}

```

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/jS2rKGecT65KWQrj5/builds/bbZvrpzt4fLh1Qfpx/openapi.json
