# PDF/UA Auto-Tagger with veraPDF Check (`madrasco/pdf-ua-auto-tagger`) Actor

Adds accessibility tags (headings, paragraphs, lists, tables, figures, links), language and title to untagged born-digital PDFs, then checks the input and output with veraPDF's PDF/UA-1 profile. Returns the tagged PDF and a before/after report.

- **URL**: https://apify.com/madrasco/pdf-ua-auto-tagger.md
- **Developed by:** [Madrasco](https://apify.com/madrasco) (community)
- **Categories:** Developer tools, Automation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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/UA Auto-Tagger with veraPDF Check

Adds the tag structure, language and title that screen readers rely on to **untagged**, born-digital PDFs, and checks the result. In our tests (2026-09-25) we tagged 14 untagged PDFs: from pdfLaTeX, LibreOffice with tagging off, a Windows print driver (Acrobat Distiller), and Chrome and Word output with its tags removed. The pages we compared (up to the first 6 of each file) looked the same before and after, and the extracted text was unchanged except in one Chrome file, where ligature text given through ActualText (for example "ff") was lost. Build 0.1.7 fixes this: when we re-checked the three Chrome files with the fixed code (2026-09-26), their extracted text was unchanged. On the 10 of these we also checked with veraPDF (LaTeX, LibreOffice, Chrome), failed PDF/UA-1 rules went from 4–13 before to 0–3 after; the rules left were figures without alt text and font problems in the original files. We have not tested the output with specific screen readers (such as NVDA, JAWS or VoiceOver); the checks below are veraPDF's automated rules. Give the actor links to PDF files; for each file it adds an accessibility **tag structure**, **document language** and **title**, then runs the open-source **veraPDF** validator with its **PDF/UA-1** profile on the file **before and after** tagging. You get the tagged PDF, a before/after row in the dataset and a readable report.

### What it does to each PDF

- Builds a structure tree from the page content: headings (H1–H6, by font size), paragraphs, lists (L/LI/Lbl/LBody), tables (Table/TR/TH/TD with header scope), figures (images, and embedded vector graphics that contain text or images) and links, in reading order (two-column pages are read column by column).
- Marks everything else (page numbers, running headers you describe with a pattern, decoration) as artifacts so screen readers skip it.
- Sets the document language, the title (shown in the viewer title bar), the PDF/UA identifier in the XMP metadata, and tab order on every page.
- Adds your alt text to figures, in page order.
- Validates the input and the output with veraPDF (PDF/UA-1 profile) and lists which rules pass and fail.

### What it does not do (read this)

- **Passing veraPDF is not the same as being accessible, and it is not a legal-compliance guarantee** (ADA, Section 508, the European Accessibility Act, WCAG or any other law or standard). veraPDF checks the machine-checkable PDF/UA-1 rules only.
- **A person still needs to review the result**: reading order, heading levels, whether tables were detected correctly, and that every image has meaningful alt text. The tagging is automatic and heuristic; on unusual layouts it can be wrong even when the checks pass.
- **Images need alt text from you.** Without it, images are tagged as figures without alt text, listed in the report, and the PDF/UA-1 check fails on them.
- **Scanned (image-only) PDFs** are not supported: they need OCR first.
- **Already-tagged PDFs keep their tags.** Re-tagging could destroy existing work, so for a PDF that already has a tag structure the actor only *finishes* the document-level metadata (input 'Already-tagged PDFs', default 'finish'; choose 'leave' to only validate): it adds the PDF/UA-1 identifier to the XMP metadata, copies the file's own title into the XMP title (or uses your 'Document title'), sets the document language from your 'Document language' input (default `en`) only if the file declares none, and turns on DisplayDocTitle and MarkInfo Marked where missing. Every change is listed in the report. It never invents a title: a file with no title of its own is reported as needing one. The existing tags (reading order, headings, tables, alt text) are **not checked or repaired**; the veraPDF after-check shows what still fails. Finished files are not charged. Many common exports are already tagged: in our tests, Chrome "Print to PDF" output, LibreOffice exports with tagging on and Word exports with accessibility tags on. On the 13 unencrypted already-tagged files in our test set (2026-09-26), the pages we compared (up to the first 6 of each file) looked the same and the extracted text was unchanged after finishing. On 14 already-tagged real-world files (2026-09-26), finish mode opened 13 (one was encrypted and refused); all document-level metadata rules passed afterwards on the 10 with a title of their own, and the other 3 were reported as needing a title. veraPDF still reports the tag-tree rules it does not touch.
- **Word output without tags** is tagged, but in our tests the text was split into many small paragraphs (often several per line) and too many lines were tagged as headings. Check the result, or export from Word with its accessibility tags option switched on.
- **Font problems** from the program that made the PDF (fonts not embedded, missing Unicode maps) are not repaired; veraPDF reports them in the after-check.
- **Text inside embedded vector graphics is not tagged.** Charts and diagrams drawn as embedded graphics (PDF form XObjects), such as LaTeX figures, are tagged as a single Figure; the labels inside them are not tagged as text. Without alt text from you, a screen reader only announces "graphic" for them, and each one is listed as a warning ("page N: embedded graphic ... was tagged as a Figure with NO alt text"). Give alt text for them in the image alt texts input.
- **Vector drawings with no text** (lines, shapes, charts whose labels are drawn as outlines) are marked as decoration and skipped by screen readers. If such a drawing carries meaning, it needs manual tagging.
- Form fields (AcroForm) and password-protected PDFs are not handled.

### Results we have measured

These are our own test results, not a promise for your files:

- RFC 9562 (46 pages, from rfc-editor.org): 12 failed PDF/UA-1 rules before, **0 after** (106 rules passed).
- Eight more RFCs in one run (RFC 9110, 9000, 9111, 9112, 9113, 9114, 9204, 8949; 35 to 194 pages each): 12 or 13 failed rules before each; 0 after for seven of them. RFC 9113 kept 1 failed rule: a diagram on page 15 is an embedded graphic, tagged as a figure without alt text and listed in the report.
- An arXiv paper made with LaTeX (arXiv 1706.03762, 15 pages, 3 images and 5 vector figures, no alt text supplied): 13 failed rules before, 3 after. The remaining failures were one rule failed by all 8 figures (no alt text supplied) and two font rules (a font not embedded, a font without a Unicode map) from the original file, which this actor does not repair. The 5 vector figures' labels (pages 13–15) are not tagged as text; the report lists each one.
- Test documents we generated with WeasyPrint (invoices, reports, tables, lists, two-column pages, images with alt text) passed veraPDF's PDF/UA-1 checks after tagging in our development tests.

### Input

- **PDF links** (required): direct links to the PDF files, reachable from the internet (for example a signed link from your storage), up to 10 per run. The default run timeout is 900 seconds; we measured about 0.7 seconds per page (9 RFCs, 714 pages, 488 seconds), so split very large batches or raise the timeout in the run options.
- **Document language** (default `en`), **Document title** (default: the PDF's own title, else the file name without `.pdf`; the report warns when the file name was used).
- **Image alt texts**: for images and embedded graphics, in page order; best with one file per run.
- **Page furniture patterns**: regular expressions for lines to mark as artifacts (default: page numbers like "Page 3 of 10").
- **Run veraPDF check** (default on), **Maximum file size** (default 100 MB).

### Output

**Dataset**, one row per file:

```json
{"fileName": "rfc9562.pdf", "status": "tagged", "pages": 46, "lang": "en",
 "title": "RFC 9562: Universally Unique IDentifiers (UUIDs)",
 "failedRulesBefore": 12, "failedRulesAfter": 0, "passesPdfUa1ChecksAfter": true,
 "structure": {"H1": 2, "H2": 18, "H3": 40, "P": 2647, "L": 56, "Table": 2, "Link": 634},
 "changes": ["Added a tag structure: ...", "Set document language ..."],
 "warnings": [], "before": {"compliant": false, "failed": [{"rule": "7.1-3", "description": "..."}]},
 "after": {"compliant": true, "passedRules": 106, "failedRules": 0},
 "taggedPdfUrl": "https://api.apify.com/v2/key-value-stores/.../records/001-rfc9562-tagged.pdf",
 "reportUrl": "https://api.apify.com/v2/key-value-stores/.../records/001-rfc9562-report.html"}
```

`status` is `tagged`, `finished` (already tagged; document-level metadata fixed, saved as `NNN-<name>-finished.pdf`), `unchanged` (already tagged and its metadata was already complete), `refused` (with `reason`: already tagged with 'leave' chosen, encrypted, unreadable), `fetch-failed` or `skipped`.

**Key-value store**: `NNN-<name>-tagged.pdf`, `NNN-<name>-report.html` and `.md` for each file, and `OUTPUT` (JSON summary).

### Privacy

Your files are downloaded into the run, processed, and stored only in your own Apify storage. Nothing is sent anywhere else.

### Support

Open an issue on the actor's Issues tab. This actor is built and maintained with AI assistance by Madrasco; a human owner can be reached on request through the Issues tab.

veraPDF is developed by the veraPDF consortium and the Open Preservation Foundation and is used unmodified under its open-source licence (GPLv3+ / MPLv2+). This actor is independent and not affiliated with or endorsed by them.

# Actor input Schema

## `pdfs` (type: `array`):

Direct links to untagged, born-digital PDFs (exported from a word processor, HTML-to-PDF tool, LaTeX, groff and similar). The links must be reachable from the internet, for example a signed link from your storage. Scanned PDFs need OCR first; already-tagged PDFs get only their document-level metadata finished (see 'Already-tagged PDFs'). Up to 10 files per run; very long PDFs may need a longer run timeout (about 0.7 s per page measured: 9 RFCs, 714 pages, 488 s).

## `language` (type: `string`):

BCP 47 language tag written into every output PDF, for example en, en-AU, de, fr-CA. For already-tagged PDFs it is used only if the file declares no language; a language the file already declares is kept.

## `title` (type: `string`):

Title shown in the viewer's title bar. Leave empty to keep the PDF's own title, or use the file name if it has none. Applies to every file in the run.

## `alreadyTagged` (type: `string`):

What to do with PDFs that already have a tag structure. 'Finish' never touches the existing tags or page content: it only adds the PDF/UA-1 identifier to the XMP metadata, copies the file's own title into the XMP title (or uses 'Document title' if you give one), sets the document language if the file declares none (from 'Document language'), and turns on DisplayDocTitle and MarkInfo Marked where missing. A file with no title of its own gets no invented title; the report asks for one. Finished files are not charged. The existing tags are not checked or repaired.

## `altTexts` (type: `array`):

Alt text for images and embedded vector graphics that contain text (charts, diagrams), in the order they appear in the document (first page first). Figures without one are tagged as figures with no alt text and reported, and the PDF/UA-1 check will fail on them. Best used with one file per run.

## `artifactPatterns` (type: `array`):

Regular expressions for text lines that are page furniture (running headers, page numbers) and should not be read aloud. Default: 'Page 3', 'Page 3 of 10' and '- 3 -'.

## `validate` (type: `boolean`):

Validate each input and output PDF with veraPDF's PDF/UA-1 profile and report the result.

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

Larger files are reported as a download failure.

## Actor input object example

```json
{
  "pdfs": [
    "https://www.rfc-editor.org/rfc/rfc9562.pdf"
  ],
  "language": "en",
  "alreadyTagged": "finish",
  "artifactPatterns": [
    "^Page \\d+( of \\d+)?$",
    "^- ?\\d+ ?-$"
  ],
  "validate": true,
  "maxFileSizeMb": 100
}
```

# Actor output Schema

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

One dataset row per PDF: veraPDF PDF/UA-1 results before and after tagging, with links to the tagged PDF and report.

## `files` (type: `string`):

The tagged PDF and a Markdown/HTML before-and-after report for each input file.

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

Counts of files given and tagged.

# 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 = {
    "pdfs": [
        "https://www.rfc-editor.org/rfc/rfc9562.pdf"
    ],
    "artifactPatterns": [
        "^Page \\d+( of \\d+)?$",
        "^- ?\\d+ ?-$"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("madrasco/pdf-ua-auto-tagger").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 = {
    "pdfs": ["https://www.rfc-editor.org/rfc/rfc9562.pdf"],
    "artifactPatterns": [
        "^Page \\d+( of \\d+)?$",
        "^- ?\\d+ ?-$",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("madrasco/pdf-ua-auto-tagger").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 '{
  "pdfs": [
    "https://www.rfc-editor.org/rfc/rfc9562.pdf"
  ],
  "artifactPatterns": [
    "^Page \\\\d+( of \\\\d+)?$",
    "^- ?\\\\d+ ?-$"
  ]
}' |
apify call madrasco/pdf-ua-auto-tagger --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,madrasco/pdf-ua-auto-tagger"
        }
    }
}
```

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/PxLuEq9ZjzF1boMfH/builds/2hI7kdBewdN9VckXa/openapi.json
