mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
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.
This commit is contained in:
@@ -126,6 +126,7 @@ TOOL_IO: dict[ToolEndpoint, ToolIOSpec] = {
|
||||
)
|
||||
],
|
||||
),
|
||||
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,
|
||||
@@ -246,6 +247,9 @@ TOOL_IO: dict[ToolEndpoint, ToolIOSpec] = {
|
||||
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,
|
||||
|
||||
@@ -11,6 +11,19 @@ from pydantic import Field, RootModel, SecretStr
|
||||
from stirling.models.base import ApiModel
|
||||
|
||||
|
||||
class Profile(StrEnum):
|
||||
"""
|
||||
Profile to check against
|
||||
"""
|
||||
|
||||
ua1 = "ua1"
|
||||
ua2 = "ua2"
|
||||
|
||||
|
||||
class AccessibilityReportParams(ApiModel):
|
||||
profile: Profile = Field(Profile.ua1, description="Profile to check against")
|
||||
|
||||
|
||||
class AddCommentsParams(ApiModel):
|
||||
comments: str = Field(
|
||||
...,
|
||||
@@ -843,11 +856,18 @@ class OutputFormat1(StrEnum):
|
||||
pdfa_2b = "pdfa-2b"
|
||||
pdfa_3 = "pdfa-3"
|
||||
pdfa_3b = "pdfa-3b"
|
||||
pdfa_1a = "pdfa-1a"
|
||||
pdfa_2a = "pdfa-2a"
|
||||
pdfa_3a = "pdfa-3a"
|
||||
pdfx = "pdfx"
|
||||
|
||||
|
||||
class PdfToPdfaParams(ApiModel):
|
||||
output_format: OutputFormat1 = Field(..., description="The output format type (PDF/A or PDF/X)")
|
||||
pdf_ua: bool = Field(
|
||||
False,
|
||||
description="Also declare PDF/UA accessibility alongside PDF/A. Only applies to the level A formats, and the claim is written only if it validates.",
|
||||
)
|
||||
strict: bool | None = Field(
|
||||
None, description="If true, the conversion will fail if the output is not perfectly compliant"
|
||||
)
|
||||
@@ -886,6 +906,65 @@ class PdfToTextParams(ApiModel):
|
||||
output_format: OutputFormat3 = Field(..., description="The output Text or RTF format")
|
||||
|
||||
|
||||
class ExistingTags(StrEnum):
|
||||
"""
|
||||
What to do with an existing structure tree: keep it, rebuild it, or decide automatically
|
||||
"""
|
||||
|
||||
auto = "auto"
|
||||
keep = "keep"
|
||||
rebuild = "rebuild"
|
||||
|
||||
|
||||
class FigurePolicy(StrEnum):
|
||||
"""
|
||||
How to treat images with no description. require-alt leaves them undescribed so the report asks for input; mark-decorative treats every image as decoration.
|
||||
"""
|
||||
|
||||
require_alt = "require-alt"
|
||||
mark_decorative = "mark-decorative"
|
||||
|
||||
|
||||
class Profile1(StrEnum):
|
||||
"""
|
||||
PDF/UA conformance level to target
|
||||
"""
|
||||
|
||||
ua1 = "ua1"
|
||||
ua2 = "ua2"
|
||||
|
||||
|
||||
class PdfToUaParams(ApiModel):
|
||||
alt_text: str | None = Field(
|
||||
None,
|
||||
description='Alternative descriptions for figures, as key=text pairs separated by newlines. Keys come from the accessibility-report endpoint\'s figuresNeedingDescription list, for example "0:12=Bar chart of quarterly revenue". Descriptions are never invented, so without these an illustrated document cannot claim conformance.',
|
||||
)
|
||||
embed_fonts: bool = Field(
|
||||
True,
|
||||
description="Embed fonts the document references but does not carry. Required for conformance and needs Ghostscript.",
|
||||
)
|
||||
existing_tags: ExistingTags = Field(
|
||||
ExistingTags.auto,
|
||||
description="What to do with an existing structure tree: keep it, rebuild it, or decide automatically",
|
||||
)
|
||||
figure_policy: FigurePolicy = Field(
|
||||
FigurePolicy.require_alt,
|
||||
description="How to treat images with no description. require-alt leaves them undescribed so the report asks for input; mark-decorative treats every image as decoration.",
|
||||
)
|
||||
language: str = Field(
|
||||
"en-GB",
|
||||
description="Document language as a BCP-47 tag, for example en-GB. Applied only when the document does not already declare one, unless overrideLanguage is set.",
|
||||
)
|
||||
override_language: bool = Field(
|
||||
False,
|
||||
description="Replace the language the document already declares. Off by default, so a document is never relabelled into a language it is not written in.",
|
||||
)
|
||||
profile: Profile1 = Field(Profile1.ua1, description="PDF/UA conformance level to target")
|
||||
title: str | None = Field(
|
||||
None, description="Document title, required by PDF/UA. Falls back to the first heading, then the filename."
|
||||
)
|
||||
|
||||
|
||||
class OutputFormat4(StrEnum):
|
||||
"""
|
||||
Target vector format extension
|
||||
@@ -1451,6 +1530,7 @@ class Model(
|
||||
| PdfToPdfaParams
|
||||
| PdfToPresentationParams
|
||||
| PdfToTextParams
|
||||
| PdfToUaParams
|
||||
| PdfToVectorParams
|
||||
| PdfToWordParams
|
||||
| PdfToXlsxParams
|
||||
@@ -1495,6 +1575,7 @@ class Model(
|
||||
| ScannerEffectParams
|
||||
| UnlockPdfFormsParams
|
||||
| UpdateMetadataParams
|
||||
| AccessibilityReportParams
|
||||
| AddPasswordParams
|
||||
| AddWatermarkParams
|
||||
| AutoRedactParams
|
||||
@@ -1525,6 +1606,7 @@ class Model(
|
||||
| PdfToPdfaParams
|
||||
| PdfToPresentationParams
|
||||
| PdfToTextParams
|
||||
| PdfToUaParams
|
||||
| PdfToVectorParams
|
||||
| PdfToWordParams
|
||||
| PdfToXlsxParams
|
||||
@@ -1569,6 +1651,7 @@ class Model(
|
||||
| ScannerEffectParams
|
||||
| UnlockPdfFormsParams
|
||||
| UpdateMetadataParams
|
||||
| AccessibilityReportParams
|
||||
| AddPasswordParams
|
||||
| AddWatermarkParams
|
||||
| AutoRedactParams
|
||||
@@ -1600,6 +1683,7 @@ type ParamToolModel = (
|
||||
| PdfToPdfaParams
|
||||
| PdfToPresentationParams
|
||||
| PdfToTextParams
|
||||
| PdfToUaParams
|
||||
| PdfToVectorParams
|
||||
| PdfToWordParams
|
||||
| PdfToXlsxParams
|
||||
@@ -1644,6 +1728,7 @@ type ParamToolModel = (
|
||||
| ScannerEffectParams
|
||||
| UnlockPdfFormsParams
|
||||
| UpdateMetadataParams
|
||||
| AccessibilityReportParams
|
||||
| AddPasswordParams
|
||||
| AddWatermarkParams
|
||||
| AutoRedactParams
|
||||
@@ -1676,6 +1761,7 @@ class ToolEndpoint(StrEnum):
|
||||
PDF_TO_PDFA = "/api/v1/convert/pdf/pdfa"
|
||||
PDF_TO_PRESENTATION = "/api/v1/convert/pdf/presentation"
|
||||
PDF_TO_TEXT = "/api/v1/convert/pdf/text"
|
||||
PDF_TO_UA = "/api/v1/convert/pdf/ua"
|
||||
PDF_TO_VECTOR = "/api/v1/convert/pdf/vector"
|
||||
PDF_TO_WORD = "/api/v1/convert/pdf/word"
|
||||
PDF_TO_XLSX = "/api/v1/convert/pdf/xlsx"
|
||||
@@ -1720,6 +1806,7 @@ class ToolEndpoint(StrEnum):
|
||||
SCANNER_EFFECT = "/api/v1/misc/scanner-effect"
|
||||
UNLOCK_PDF_FORMS = "/api/v1/misc/unlock-pdf-forms"
|
||||
UPDATE_METADATA = "/api/v1/misc/update-metadata"
|
||||
ACCESSIBILITY_REPORT = "/api/v1/security/accessibility-report"
|
||||
ADD_PASSWORD = "/api/v1/security/add-password"
|
||||
ADD_WATERMARK = "/api/v1/security/add-watermark"
|
||||
AUTO_REDACT = "/api/v1/security/auto-redact"
|
||||
@@ -1750,6 +1837,7 @@ OPERATIONS: dict[ToolEndpoint, ParamToolModelType] = {
|
||||
ToolEndpoint.PDF_TO_PDFA: PdfToPdfaParams,
|
||||
ToolEndpoint.PDF_TO_PRESENTATION: PdfToPresentationParams,
|
||||
ToolEndpoint.PDF_TO_TEXT: PdfToTextParams,
|
||||
ToolEndpoint.PDF_TO_UA: PdfToUaParams,
|
||||
ToolEndpoint.PDF_TO_VECTOR: PdfToVectorParams,
|
||||
ToolEndpoint.PDF_TO_WORD: PdfToWordParams,
|
||||
ToolEndpoint.PDF_TO_XLSX: PdfToXlsxParams,
|
||||
@@ -1794,6 +1882,7 @@ OPERATIONS: dict[ToolEndpoint, ParamToolModelType] = {
|
||||
ToolEndpoint.SCANNER_EFFECT: ScannerEffectParams,
|
||||
ToolEndpoint.UNLOCK_PDF_FORMS: UnlockPdfFormsParams,
|
||||
ToolEndpoint.UPDATE_METADATA: UpdateMetadataParams,
|
||||
ToolEndpoint.ACCESSIBILITY_REPORT: AccessibilityReportParams,
|
||||
ToolEndpoint.ADD_PASSWORD: AddPasswordParams,
|
||||
ToolEndpoint.ADD_WATERMARK: AddWatermarkParams,
|
||||
ToolEndpoint.AUTO_REDACT: AutoRedactParams,
|
||||
|
||||
Reference in New Issue
Block a user