# Tagged PDF (PDF/UA) Publish Conformance Gate (`kingii98/tagged-pdf-pdf-ua-publish-conformance-gate`) Actor

Audit the PDF files that you publish against the tagged-document rules of PDF/UA-1, PDF/UA-2 or WCAG 2.2. The Actor downloads each file once, reads its structure tree, and reports every untagged item, every figure without alternative text, every table wit

- **URL**: https://apify.com/kingii98/tagged-pdf-pdf-ua-publish-conformance-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 run\_starts

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

## Tagged PDF (PDF/UA) Publish Conformance Gate

Audit the PDF files that you publish against the tagged-document rules of
PDF/UA-1, PDF/UA-2 or WCAG 2.2, before the file goes on the website.

Give the Actor a bounded list of PDF URLs. The Actor downloads each file once,
reads its structure tree, and writes one dataset row for each file plus one gate
summary row. It does not crawl, it does not log in, it sends no credential, and
it uses no browser and no proxy.

A failed gate is a result, not a failed run. The run always ends with success,
and the verdict is in the dataset and in the run status message.

### Input

| Field | Type | Default | Function |
| --- | --- | --- | --- |
| `pdf_urls` | array of strings | three public example files | 1 to 500 absolute HTTPS URLs of PDF files. |
| `profile` | string | `PDF/UA-1` | `PDF/UA-1`, `PDF/UA-2` or `WCAG-2.2-document`. |
| `max_pages_per_file` | integer | `200` | How many pages of one file the Actor reads. |
| `fail_on` | array of strings | `["error"]` | The severities that give a file the verdict `fail`. |
| `timeout_seconds_per_file` | integer | `60` | Download timeout of one file. |

Every field has a default, so a run with empty input audits the public example
fixture in `.actor/default_input.json`.

### Output

One row for each file, with `record_type` `file`:

`file_url`, `http_status`, `bytes`, `page_count`, `pages_audited`,
`is_encrypted`, `is_tagged`, `tag_tree_depth`, `document_language_set`,
`title_set`, `display_doc_title_flag`, `images_total`,
`images_without_alt_text`, `tables_without_header_scope`,
`headings_out_of_order`, `untagged_content_items`, `reading_order_conflicts`,
`form_fields_without_label`, `findings[]` and `verdict`
(`pass`, `fail` or `unreadable`).

Each entry of `findings[]` holds `rule_id`, `severity`, `page`, `object_path`
and `message`.

One summary row, with `record_type` `summary`: `files_requested`,
`files_audited`, `files_pass`, `files_fail`, `files_unreadable`,
`findings_by_rule`, `findings_by_severity` and `gate_result`.

### Rules

The profile sets the severity of each rule. A rule that a profile does not
state is not applied.

| Rule | PDF/UA-1 | PDF/UA-2 | WCAG 2.2 | Function |
| --- | --- | --- | --- | --- |
| `PDFUA-TAGGED` | error | error | error | The document has `/MarkInfo /Marked true` and a `/StructTreeRoot`. |
| `PDFUA-LANG` | error | error | error | The document states a default language. |
| `PDFUA-TITLE` | error | error | error | The document states a title in XMP `dc:title` or in `/Info /Title`. |
| `PDFUA-DISPLAY-TITLE` | error | error | warning | `/ViewerPreferences /DisplayDocTitle` is true, so the reader shows the title and not the file name. |
| `PDFUA-XMP-IDENT` | error | error | — | The XMP metadata states the `pdfuaid:part` of the profile. |
| `PDFUA-FIGURE-ALT` | error | error | error | Each figure holds alternative text. |
| `PDFUA-UNTAGGED-CONTENT` | error | error | error | No content is outside the structure tree and unmarked as an artifact. |
| `PDFUA-TABLE-HEADERS` | error | error | error | Each table holds header cells, and each header cell states its scope. |
| `PDFUA-HEADING-ORDER` | error | error | warning | The heading levels go down one step at a time. |
| `PDFUA-FORM-LABEL` | error | error | error | Each form field holds a label or a tooltip. |
| `PDFUA-READING-ORDER` | warning | warning | warning | The structure order agrees with the content order of the page. |
| `PDFUA-TAG-DEPTH` | warning | warning | warning | The structure tree is more than one level deep. |
| `PDFUA-OUTLINE` | — | — | warning | A document of 10 pages or more holds a bookmark list. |
| `PDFUA-PAGES-TRUNCATED` | info | info | info | The page cap of the run stopped the audit before the last page. |
| `PDFUA-UNREADABLE` | error | error | error | The Actor could not download the file or could not read its structure. |

### Pricing

The Actor uses the pay-per-event model. It charges these three events:

| Event | Unit | Function |
| --- | --- | --- |
| `run_start` | one for each run | The container start and the input read. Charged once, before the Actor reads the input. |
| `pdf_audited` | one for each file that the Actor reads and audits | The download and the structure parse of one file. A file that the Actor could not download is not charged. |
| `page_batch_over_50` | one for each 100 pages above the first 50 pages of a file | The extra parse time of a long document. A file of 50 pages or fewer charges none. |

Each event is charged once for the whole run, with the total count. The Actor
never charges more than the run's maximum total charge allows.

### Limits

- 1 to 500 URLs for each run, HTTPS only, no credentials in the URL.
- Loopback, private and reserved addresses are refused, and every redirect hop
  is checked again.
- 3 redirect hops, 32 MB for each file, 5 parallel downloads.
- 1 to 5000 pages for each file, 5 to 300 seconds for each download.

### Development

```
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `pdf_urls` (type: `array`):

Absolute HTTPS URLs of 1 to 500 PDF files. The Actor downloads each file once and reads its structure. It does not crawl, it does not log in, it sends no credential, and it uses no browser and no proxy. The list holds the three public example files until you replace them.

## `profile` (type: `string`):

Which rule set the gate applies. 'PDF/UA-1' (ISO 14289-1) and 'PDF/UA-2' (ISO 14289-2) also check the XMP pdfuaid:part identifier and treat a missing document title flag as an error. 'WCAG-2.2-document' drops the identifier rule, lowers the title flag and the heading order to warnings, and adds a bookmark rule for a long document. The profile sets the severity of each rule, so it sets the gate result.

## `max_pages_per_file` (type: `integer`):

How many pages of one file the Actor reads. A longer file keeps its full page\_count, and it reports the PDFUA-PAGES-TRUNCATED finding. This bound holds the run time and the cost of a large document.

## `fail_on` (type: `array`):

A file gets the verdict 'fail' when it holds a finding of one of these severities. Give an empty list to get a report without a gate: each readable file then passes, and only a file that the Actor cannot read fails the run gate.

## `timeout_seconds_per_file` (type: `integer`):

Per-request timeout of the download of one PDF file. A file that exceeds it gets the verdict 'unreadable', and the run continues with the other files.

## Actor input object example

```json
{
  "pdf_urls": [
    "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.1%20General/7.1-t01-pass-a.pdf",
    "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.3%20Graphics/7.3-t01-fail-a.pdf",
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "profile": "PDF/UA-1",
  "max_pages_per_file": 200,
  "fail_on": [
    "error"
  ],
  "timeout_seconds_per_file": 60
}
```

# Actor output Schema

## `dataset` (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 = {
    "pdf_urls": [
        "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.1%20General/7.1-t01-pass-a.pdf",
        "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.3%20Graphics/7.3-t01-fail-a.pdf",
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ],
    "profile": "PDF/UA-1",
    "max_pages_per_file": 200,
    "fail_on": [
        "error"
    ],
    "timeout_seconds_per_file": 60
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/tagged-pdf-pdf-ua-publish-conformance-gate").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 = {
    "pdf_urls": [
        "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.1%20General/7.1-t01-pass-a.pdf",
        "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.3%20Graphics/7.3-t01-fail-a.pdf",
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    ],
    "profile": "PDF/UA-1",
    "max_pages_per_file": 200,
    "fail_on": ["error"],
    "timeout_seconds_per_file": 60,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/tagged-pdf-pdf-ua-publish-conformance-gate").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 '{
  "pdf_urls": [
    "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.1%20General/7.1-t01-pass-a.pdf",
    "https://raw.githubusercontent.com/veraPDF/veraPDF-corpus/01e40281d48e2f3755006fdf596ca25caaea8634/PDF_UA-1/7.3%20Graphics/7.3-t01-fail-a.pdf",
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "profile": "PDF/UA-1",
  "max_pages_per_file": 200,
  "fail_on": [
    "error"
  ],
  "timeout_seconds_per_file": 60
}' |
apify call kingii98/tagged-pdf-pdf-ua-publish-conformance-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/tagged-pdf-pdf-ua-publish-conformance-gate"
        }
    }
}

```

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/ijhxXiBO2onuAO1by/builds/Bxbd6xeCMc2uWNiBS/openapi.json
