Files
Stirling-PDF/engine/src/stirling/models/tool_io.py
T
Anthony Stirling 96a00cebd1 Pdf ua converter testing (#7301)
# Description of Changes

Adds a PDF/UA converter, an accessibility report, and PDF/A conformance
level A.

**New: `POST /api/v1/convert/pdf/ua`** (Convert tool, "PDF/UA" target).
Tags an untagged PDF, marks
decorative content as artifacts, embeds missing fonts and applies the
document-level PDF/UA
requirements (title, language, tab order, form-field descriptions), then
validates with veraPDF. The
`pdfuaid` declaration is written only if validation passes, so a
returned file never claims more
than it delivers; response headers report whether it was declared, how
many checks still fail and
how many images still need a description.

**New: `POST /api/v1/security/accessibility-report`.** Reports what
fails, what the converter can fix
on its own, what needs a person, and lists the figures needing a
description with the keys the
conversion accepts back. Read-only; does not modify the file. Capped at
100 MB / 2000 pages and
weighted `LARGE_WEIGHT`, since it runs a full veraPDF pass plus the
converter's layout analysis over
every page.

**PDF/A level A.** `pdfa-1a`, `pdfa-2a` and `pdfa-3a` output formats on
the existing
`/api/v1/convert/pdf/pdfa` endpoint. Level A is level B plus tagging, so
the document is tagged
after Ghostscript (which discards any structure tree it is given) and
the level A claim is written
only if veraPDF agrees. Optional `pdfUa=true` additionally declares
PDF/UA alongside PDF/A, again
only if it validates.

Honesty rules the implementation holds to:

- **Never claim a level that was not reached.** If tagging fails, the
file is returned at level B and
is named `_PDFA-2b.pdf`, not `_PDFA-2a.pdf`. With `strict=true` the
request fails outright rather
than returning a level B file against a level A request, and a level B
pass no longer satisfies a
  strict level A request.
- **Never relabel a document's language.** The requested language
(default `en-GB`) is applied only
when the document declares none; a French PDF stays French unless the
caller sets
`overrideLanguage`, and ignoring a requested language is reported as a
warning.
- **Never invent alternative text.** Descriptions come from the caller.
The Convert panel can list
the images needing one (via the report endpoint) and send them back per
figure; any image left
undescribed blocks the conformance claim rather than being papered over.
- **Never certify hidden content.** Marking images decorative, or
suppressing text that could not be
tagged reliably, withdraws the claim instead of passing the checker by
hiding content.

PDF/UA-1 and PDF/UA-2 are both offered; UA-2 raises the file to PDF 2.0
and namespaces the structure
tree, and its test asserts conformance rather than merely reporting it.

Convert steps saved in Automations/Pipelines round-trip their PDF/UA
settings (profile, language,
override, title, font embedding, descriptions).

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-08-20 12:00:03 +00:00

279 lines
13 KiB
Python

# AUTO-GENERATED FILE. DO NOT EDIT.
# Generated by scripts/generate_tool_models.py from the Java OpenAPI spec (SwaggerDoc.json).
# Regenerate with: task engine:tool-models
"""What each tool endpoint accepts and produces, so a planned chain can be checked before it
is run. Declared in Java with ``@ToolIO``; see ``stirling.services.tool_io_compat`` for the
compatibility rules that read this table."""
from enum import StrEnum
from pydantic import Field
from stirling.models.base import ApiModel
from stirling.models.tool_models import ToolEndpoint
class ToolFormat(StrEnum):
"""The kind of file a tool consumes or produces. ``ANY`` accepts or produces anything;
``NONE`` means no file at all, such as a report or a status."""
PDF = "PDF"
PDF_ENCRYPTED = "PDF_ENCRYPTED"
IMAGE = "IMAGE"
ZIP = "ZIP"
WORD = "WORD"
PPT = "PPT"
EXCEL = "EXCEL"
CSV = "CSV"
HTML = "HTML"
XML = "XML"
JSON = "JSON"
TEXT = "TEXT"
MARKDOWN = "MARKDOWN"
JAVASCRIPT = "JAVASCRIPT"
EBOOK = "EBOOK"
EMAIL = "EMAIL"
POSTSCRIPT = "POSTSCRIPT"
PCL = "PCL"
XPS = "XPS"
VIDEO = "VIDEO"
CBZ = "CBZ"
CBR = "CBR"
ANY = "ANY"
NONE = "NONE"
class ToolArity(StrEnum):
"""How many files go in and come out (Single/Multiple In, Single/Multiple Out). A
multi-output tool returns its results zipped, and the caller unpacks them."""
SISO = "SISO"
SIMO = "SIMO"
MISO = "MISO"
MIMO = "MIMO"
class ToolIOWhen(ApiModel):
"""One condition on a request parameter, guarding a :class:`ToolIOCase`."""
param: str
matches: list[str]
# The value the endpoint uses when this parameter is absent; None when it has none.
default: str | None = None
class ToolIOCase(ApiModel):
"""An output that applies when every condition in ``when`` holds."""
when: list[ToolIOWhen]
produces: ToolFormat
arity: ToolArity
class ToolIOSpec(ApiModel):
"""What one endpoint accepts and produces."""
accepts: list[ToolFormat]
produces: ToolFormat
arity: ToolArity
cases: list[ToolIOCase] = Field(default_factory=list)
TOOL_IO: dict[ToolEndpoint, ToolIOSpec] = {
ToolEndpoint.CBR_TO_PDF: ToolIOSpec(accepts=[ToolFormat.CBR], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.CBZ_TO_PDF: ToolIOSpec(accepts=[ToolFormat.CBZ], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.EBOOK_TO_PDF: ToolIOSpec(accepts=[ToolFormat.EBOOK], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.EML_TO_PDF: ToolIOSpec(accepts=[ToolFormat.EMAIL], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.FILE_TO_PDF: ToolIOSpec(accepts=[ToolFormat.ANY], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.HTML_TO_PDF: ToolIOSpec(
accepts=[ToolFormat.HTML, ToolFormat.ZIP], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.IMG_TO_PDF: ToolIOSpec(accepts=[ToolFormat.IMAGE], produces=ToolFormat.PDF, arity=ToolArity.MISO),
ToolEndpoint.MARKDOWN_TO_PDF: ToolIOSpec(
accepts=[ToolFormat.MARKDOWN, ToolFormat.ZIP], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.PDF_TO_CBR: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.CBR, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_CBZ: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.CBZ, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_CSV: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.CSV, arity=ToolArity.SIMO),
ToolEndpoint.PDF_TO_EPUB: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.EBOOK, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_HTML: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.ZIP, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_IMG: ToolIOSpec(
accepts=[ToolFormat.PDF],
produces=ToolFormat.IMAGE,
arity=ToolArity.SIMO,
cases=[
ToolIOCase(
when=[ToolIOWhen(param="singleOrMultiple", matches=["single"], default="multiple")],
produces=ToolFormat.IMAGE,
arity=ToolArity.SISO,
)
],
),
ToolEndpoint.PDF_TO_MARKDOWN: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.MARKDOWN, arity=ToolArity.SISO
),
ToolEndpoint.PDF_TO_PDFA: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_PRESENTATION: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PPT, arity=ToolArity.SISO
),
ToolEndpoint.PDF_TO_TEXT: ToolIOSpec(
accepts=[ToolFormat.PDF],
produces=ToolFormat.TEXT,
arity=ToolArity.SISO,
cases=[
ToolIOCase(
when=[ToolIOWhen(param="outputFormat", matches=["rtf"])], produces=ToolFormat.WORD, arity=ToolArity.SISO
)
],
),
ToolEndpoint.PDF_TO_UA: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_VECTOR: ToolIOSpec(
accepts=[ToolFormat.PDF],
produces=ToolFormat.IMAGE,
arity=ToolArity.SISO,
cases=[
ToolIOCase(
when=[ToolIOWhen(param="outputFormat", matches=["ps"], default="eps")],
produces=ToolFormat.POSTSCRIPT,
arity=ToolArity.SISO,
),
ToolIOCase(
when=[ToolIOWhen(param="outputFormat", matches=["pcl"], default="eps")],
produces=ToolFormat.PCL,
arity=ToolArity.SISO,
),
ToolIOCase(
when=[ToolIOWhen(param="outputFormat", matches=["xps"], default="eps")],
produces=ToolFormat.XPS,
arity=ToolArity.SISO,
),
],
),
ToolEndpoint.PDF_TO_WORD: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.WORD, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_XLSX: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.EXCEL, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_XML: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.XML, arity=ToolArity.SISO),
ToolEndpoint.SVG_TO_PDF: ToolIOSpec(
accepts=[ToolFormat.IMAGE],
produces=ToolFormat.PDF,
arity=ToolArity.MIMO,
cases=[
ToolIOCase(
when=[ToolIOWhen(param="combineIntoSinglePdf", matches=["true"], default="false")],
produces=ToolFormat.PDF,
arity=ToolArity.MISO,
)
],
),
ToolEndpoint.URL_TO_PDF: ToolIOSpec(accepts=[ToolFormat.NONE], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.VECTOR_TO_PDF: ToolIOSpec(
accepts=[ToolFormat.POSTSCRIPT], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.BOOKLET_IMPOSITION: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.CROP: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.EDIT_TABLE_OF_CONTENTS: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.EDIT_TEXT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.MERGE_PDFS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.MISO),
ToolEndpoint.MULTI_PAGE_LAYOUT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.PDF_TO_SINGLE_PAGE: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.REARRANGE_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REMOVE_IMAGE_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REMOVE_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.ROTATE_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.SCALE_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.SPLIT_BY_SIZE_OR_COUNT: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO
),
ToolEndpoint.SPLIT_FOR_POSTER_PRINT: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO
),
ToolEndpoint.SPLIT_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO),
ToolEndpoint.SPLIT_PDF_BY_CHAPTERS: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO
),
ToolEndpoint.SPLIT_PDF_BY_SECTIONS: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO
),
ToolEndpoint.ADD_COMMENTS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.ADD_PAGE_NUMBERS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.ADD_STAMP: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.AUTO_RENAME: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.AUTO_ROTATE_PDF: ToolIOSpec(
accepts=[ToolFormat.PDF],
produces=ToolFormat.PDF,
arity=ToolArity.SISO,
cases=[
ToolIOCase(
when=[ToolIOWhen(param="dryRun", matches=["true"], default="false")],
produces=ToolFormat.JSON,
arity=ToolArity.SISO,
)
],
),
ToolEndpoint.AUTO_SPLIT_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO),
ToolEndpoint.COMPRESS_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.DELETE_ATTACHMENT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.EXTRACT_ATTACHMENTS: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.ZIP, arity=ToolArity.SISO
),
ToolEndpoint.EXTRACT_IMAGE_SCANS: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.IMAGE, arity=ToolArity.SIMO
),
ToolEndpoint.EXTRACT_IMAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.IMAGE, arity=ToolArity.SIMO),
ToolEndpoint.FLATTEN: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.OCR_PDF: ToolIOSpec(
accepts=[ToolFormat.PDF],
produces=ToolFormat.PDF,
arity=ToolArity.SISO,
cases=[
ToolIOCase(
when=[ToolIOWhen(param="sidecar", matches=["true"], default="false")],
produces=ToolFormat.ZIP,
arity=ToolArity.SISO,
)
],
),
ToolEndpoint.REMOVE_BLANKS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO),
ToolEndpoint.RENAME_ATTACHMENT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REPAIR: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REPLACE_INVERT_PDF: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.SCANNER_EFFECT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.UNLOCK_PDF_FORMS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.UPDATE_METADATA: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.ACCESSIBILITY_REPORT: ToolIOSpec(
accepts=[ToolFormat.PDF], produces=ToolFormat.JSON, arity=ToolArity.SISO
),
ToolEndpoint.ADD_PASSWORD: ToolIOSpec(
accepts=[ToolFormat.PDF],
produces=ToolFormat.PDF_ENCRYPTED,
arity=ToolArity.SISO,
cases=[
ToolIOCase(
when=[
ToolIOWhen(param="password", matches=[""], default=""),
ToolIOWhen(param="ownerPassword", matches=[""], default=""),
],
produces=ToolFormat.PDF,
arity=ToolArity.SISO,
)
],
),
ToolEndpoint.ADD_WATERMARK: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.AUTO_REDACT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REDACT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REDACT_EXECUTE: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REMOVE_CERT_SIGN: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.REMOVE_PASSWORD: ToolIOSpec(
accepts=[ToolFormat.PDF, ToolFormat.PDF_ENCRYPTED], produces=ToolFormat.PDF, arity=ToolArity.SISO
),
ToolEndpoint.SANITIZE_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
ToolEndpoint.TIMESTAMP_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO),
}