diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md
index 17971c59d6..ca7105bfbf 100644
--- a/news/changelog-1.9.md
+++ b/news/changelog-1.9.md
@@ -40,10 +40,13 @@ All changes included in 1.9:
- ([#13589](https://github.com/quarto-dev/quarto-cli/issues/13589)): Fix callouts with invalid ID prefixes crashing with "attempt to index a nil value". Callouts with unknown reference types now render as non-crossreferenceable callouts with a warning, ignoring the invalid ID.
- ([#13602](https://github.com/quarto-dev/quarto-cli/issues/13602)): Fix support for multiple files set in `bibliography` field in `biblio.typ` template partial.
- ([#13775](https://github.com/quarto-dev/quarto-cli/issues/13775)): Fix brand fonts not being applied when using `citeproc: true` with Typst format. Format detection now properly handles Pandoc format variants like `typst-citations`.
+- ([#13868](https://github.com/quarto-dev/quarto-cli/issues/13868)): Add image alt text support for PDF/UA accessibility. Alt text from markdown captions and explicit `alt` attributes is now passed to Typst's `image()` function. (Temporary workaround until [jgm/pandoc#11394](https://github.com/jgm/pandoc/pull/11394) is merged.)
### `pdf`
+- ([#4426](https://github.com/quarto-dev/quarto-cli/issues/4426)): Add `pdf-standard` option for PDF/A, PDF/UA, and PDF version control. Supports standards like `a-2b`, `ua-1`, and versions `1.7`, `2.0`. Works with both LaTeX and Typst formats.
- ([#10291](https://github.com/quarto-dev/quarto-cli/issues/10291)): Fix detection of babel hyphenation warnings with straight-quote format instead of backtick-quote format.
+- ([#13248](https://github.com/quarto-dev/quarto-cli/issues/13248)): Fix image alt text not being passed to LaTeX `\includegraphics[alt={...}]` for PDF accessibility. Markdown image captions and `fig-alt` attributes are now preserved for PDF/UA compliance.
- ([#13661](https://github.com/quarto-dev/quarto-cli/issues/13661)): Fix LaTeX compilation errors when using `mermaid-format: svg` with PDF/LaTeX output. SVG diagrams are now written directly without HTML script tags. Note: `mermaid-format: png` is recommended for best compatibility. SVG format requires `rsvg-convert` (or Inkscape with `use-rsvg-convert: false`) in PATH for conversion to PDF, and may experience text clipping in diagrams with multi-line labels.
- ([rstudio/tinytex-releases#49](https://github.com/rstudio/tinytex-releases/issues/49)): Fix detection of LuaTeX-ja missing file errors by matching both "File" and "file" in error messages.
- ([#13667](https://github.com/quarto-dev/quarto-cli/issues/13667)): Fix LaTeX compilation error with Python error output containing caret characters.
diff --git a/src/command/render/latexmk/parse-error.ts b/src/command/render/latexmk/parse-error.ts
index 22ff6d1857..9445930967 100644
--- a/src/command/render/latexmk/parse-error.ts
+++ b/src/command/render/latexmk/parse-error.ts
@@ -107,6 +107,53 @@ const resolvingMatchers = [
},
];
+// Finds PDF/UA accessibility warnings from tagpdf and DocumentMetadata
+export interface PdfAccessibilityWarnings {
+ missingAltText: string[]; // filenames of images missing alt text
+ missingLanguage: boolean; // document language not set
+ otherWarnings: string[]; // other tagpdf warnings
+}
+
+export function findPdfAccessibilityWarnings(
+ logText: string,
+): PdfAccessibilityWarnings {
+ const result: PdfAccessibilityWarnings = {
+ missingAltText: [],
+ missingLanguage: false,
+ otherWarnings: [],
+ };
+
+ // Match: Package tagpdf Warning: Alternative text for graphic is missing.
+ // (tagpdf) Using 'filename' instead.
+ const altTextRegex =
+ /Package tagpdf Warning: Alternative text for graphic is missing\.\s*\n\(tagpdf\)\s*Using ['`]([^'`]+)['`] instead\./g;
+ let match;
+ while ((match = altTextRegex.exec(logText)) !== null) {
+ result.missingAltText.push(match[1]);
+ }
+
+ // Match: LaTeX DocumentMetadata Warning: The language has not been set in
+ if (
+ /LaTeX DocumentMetadata Warning: The language has not been set in/.test(
+ logText,
+ )
+ ) {
+ result.missingLanguage = true;
+ }
+
+ // Capture any other tagpdf warnings we haven't specifically handled
+ const otherTagpdfRegex = /Package tagpdf Warning: ([^\n]+)/g;
+ while ((match = otherTagpdfRegex.exec(logText)) !== null) {
+ const warning = match[1];
+ // Skip the alt text warning we already handle specifically
+ if (!warning.startsWith("Alternative text for graphic is missing")) {
+ result.otherWarnings.push(warning);
+ }
+ }
+
+ return result;
+}
+
// Finds missing hyphenation files (these appear as warnings in the log file)
export function findMissingHyphenationFiles(logText: string) {
//ngerman gets special cased
@@ -273,6 +320,19 @@ const packageMatchers = [
return "colorprofiles.sty";
},
},
+ {
+ regex: /.*No support files for \\DocumentMetadata found.*/g,
+ filter: (_match: string, _text: string) => {
+ return "latex-lab";
+ },
+ },
+ {
+ // PDF/A requires embedded color profiles - pdfmanagement-testphase needs colorprofiles
+ regex: /.*\(pdf backend\): cannot open file for embedding.*/g,
+ filter: (_match: string, _text: string) => {
+ return "colorprofiles";
+ },
+ },
{
regex: /.*No file ([^`'. ]+[.]fd)[.].*/g,
filter: (match: string, _text: string) => {
@@ -297,7 +357,7 @@ const packageMatchers = [
];
function fontSearchTerm(font: string): string {
- const fontPattern = font.replace(/\s+/g, '\\s*');
+ const fontPattern = font.replace(/\s+/g, "\\s*");
return `${fontPattern}(-(Bold|Italic|Regular).*)?[.](tfm|afm|mf|otf|ttf)`;
}
diff --git a/src/command/render/latexmk/pdf.ts b/src/command/render/latexmk/pdf.ts
index 7aa66aa942..9af74e170f 100644
--- a/src/command/render/latexmk/pdf.ts
+++ b/src/command/render/latexmk/pdf.ts
@@ -21,6 +21,7 @@ import {
findLatexError,
findMissingFontsAndPackages,
findMissingHyphenationFiles,
+ findPdfAccessibilityWarnings,
kMissingFontLog,
needsRecompilation,
} from "./parse-error.ts";
@@ -197,6 +198,25 @@ async function initialCompileLatex(
`Possibly missing hyphenation file: '${missingHyphenationFile}'. See more in logfile (by setting 'latex-clean: false').\n`,
);
}
+
+ // Check for accessibility warnings (e.g., missing alt text, language with PDF/UA)
+ const accessibilityWarnings = findPdfAccessibilityWarnings(logText);
+ if (accessibilityWarnings.missingAltText.length > 0) {
+ const fileList = accessibilityWarnings.missingAltText.join(", ");
+ warning(
+ `PDF accessibility: Missing alt text for image(s): ${fileList}. Add alt text using  syntax for PDF/UA compliance.\n`,
+ );
+ }
+ if (accessibilityWarnings.missingLanguage) {
+ warning(
+ `PDF accessibility: Document language not set. Add 'lang: en' (or appropriate language) to document metadata for PDF/UA compliance.\n`,
+ );
+ }
+ if (accessibilityWarnings.otherWarnings.length > 0) {
+ for (const warn of accessibilityWarnings.otherWarnings) {
+ warning(`PDF accessibility: ${warn}\n`);
+ }
+ }
} else if (pkgMgr.autoInstall) {
// try autoinstalling
// First be sure all packages are up to date
diff --git a/src/command/render/latexmk/texlive.ts b/src/command/render/latexmk/texlive.ts
index 2a1884155a..923e642196 100644
--- a/src/command/render/latexmk/texlive.ts
+++ b/src/command/render/latexmk/texlive.ts
@@ -86,10 +86,11 @@ export async function findPackages(
`finding package for ${searchTerm}`,
);
}
- // Special case for a known package
+ // Special cases for known packages where tlmgr file search doesn't work
// https://github.com/rstudio/tinytex/blob/33cbe601ff671fae47c594250de1d22bbf293b27/R/latex.R#L470
- if (searchTerm === "fandol") {
- results.push("fandol");
+ const knownPackages = ["fandol", "latex-lab", "colorprofiles"];
+ if (knownPackages.includes(searchTerm)) {
+ results.push(searchTerm);
} else {
const result = await tlmgrCommand(
"search",
diff --git a/src/command/render/output-typst.ts b/src/command/render/output-typst.ts
index 83a5b3f9ca..55e2712f2d 100644
--- a/src/command/render/output-typst.ts
+++ b/src/command/render/output-typst.ts
@@ -12,8 +12,10 @@ import {
kKeepTyp,
kOutputExt,
kOutputFile,
+ kPdfStandard,
kVariant,
} from "../../config/constants.ts";
+import { warning } from "../../deno_ral/log.ts";
import { Format } from "../../config/types.ts";
import { writeFileToStdout } from "../../core/console.ts";
import { dirAndStem, expandPath } from "../../core/path.ts";
@@ -66,6 +68,11 @@ export function typstPdfOutputRecipe(
const typstOptions: TypstCompileOptions = {
quiet: options.flags?.quiet,
fontPaths: asArray(format.metadata?.[kFontPaths]) as string[],
+ pdfStandard: normalizePdfStandardForTypst(
+ asArray(
+ format.render?.[kPdfStandard] ?? format.metadata?.[kPdfStandard],
+ ),
+ ),
};
if (project?.dir) {
typstOptions.rootDir = project.dir;
@@ -140,3 +147,41 @@ export function typstPdfOutputRecipe(
return recipe;
}
+
+// Typst-supported PDF standards
+const kTypstSupportedStandards = new Set([
+ "1.4",
+ "1.5",
+ "1.6",
+ "1.7",
+ "2.0",
+ "a-1b",
+ "a-1a",
+ "a-2b",
+ "a-2u",
+ "a-2a",
+ "a-3b",
+ "a-3u",
+ "a-3a",
+ "a-4",
+ "a-4f",
+ "a-4e",
+ "ua-1",
+]);
+
+function normalizePdfStandardForTypst(standards: unknown[]): string[] {
+ const result: string[] = [];
+ for (const s of standards) {
+ if (typeof s !== "string") continue;
+ // Normalize: lowercase, remove any "pdf" prefix
+ const normalized = s.toLowerCase().replace(/^pdf[/-]?/, "");
+ if (kTypstSupportedStandards.has(normalized)) {
+ result.push(normalized);
+ } else {
+ warning(
+ `PDF standard '${s}' is not supported by Typst and will be ignored`,
+ );
+ }
+ }
+ return result;
+}
diff --git a/src/config/constants.ts b/src/config/constants.ts
index 56e75b1d82..2a64c4193f 100644
--- a/src/config/constants.ts
+++ b/src/config/constants.ts
@@ -86,6 +86,7 @@ export const kShortcodes = "shortcodes";
export const kKeepMd = "keep-md";
export const kKeepTex = "keep-tex";
export const kKeepTyp = "keep-typ";
+export const kPdfStandard = "pdf-standard";
export const kKeepIpynb = "keep-ipynb";
export const kKeepSource = "keep-source";
export const kVariant = "variant";
@@ -219,6 +220,7 @@ export const kRenderDefaultsKeys = [
kLatexTlmgrOpts,
kLatexOutputDir,
kLatexTinyTex,
+ kPdfStandard,
kLinkExternalIcon,
kLinkExternalNewwindow,
kLinkExternalFilter,
@@ -686,6 +688,7 @@ export const kPandocDefaultsKeys = [
kPdfEngine,
kPdfEngineOpts,
kPdfEngineOpt,
+ kPdfStandard,
kWrap,
kColumns,
"dpi",
diff --git a/src/config/types.ts b/src/config/types.ts
index 09daafe951..1f7c3132a9 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -176,6 +176,7 @@ import {
kPdfEngine,
kPdfEngineOpt,
kPdfEngineOpts,
+ kPdfStandard,
kPlotlyConnected,
kPreferHtml,
kPreserveYaml,
@@ -492,6 +493,7 @@ export interface FormatRender {
[kLatexMinRuns]?: number;
[kLatexMaxRuns]?: number;
[kLatexClean]?: boolean;
+ [kPdfStandard]?: string | string[];
[kLatexInputPaths]?: string[];
[kLatexMakeIndex]?: string;
[kLatexMakeIndexOpts]?: string[];
diff --git a/src/core/typst.ts b/src/core/typst.ts
index 09e95cab99..0003463ee5 100644
--- a/src/core/typst.ts
+++ b/src/core/typst.ts
@@ -39,6 +39,7 @@ export type TypstCompileOptions = {
quiet?: boolean;
fontPaths?: string[];
rootDir?: string;
+ pdfStandard?: string[];
};
export async function typstCompile(
@@ -58,6 +59,9 @@ export async function typstCompile(
if (options.rootDir) {
cmd.push("--root", options.rootDir);
}
+ if (options.pdfStandard && options.pdfStandard.length > 0) {
+ cmd.push("--pdf-standard", options.pdfStandard.join(","));
+ }
cmd.push(
input,
...fontPathsArgs(fontPaths),
diff --git a/src/format/pdf/format-pdf.ts b/src/format/pdf/format-pdf.ts
index 4e80f00d55..9b9facfaf4 100644
--- a/src/format/pdf/format-pdf.ts
+++ b/src/format/pdf/format-pdf.ts
@@ -31,12 +31,15 @@ import {
kNumberSections,
kPaperSize,
kPdfEngine,
+ kPdfStandard,
kReferenceLocation,
kShiftHeadingLevelBy,
kTblCapLoc,
kTopLevelDivision,
kWarning,
} from "../../config/constants.ts";
+import { warning } from "../../deno_ral/log.ts";
+import { asArray } from "../../core/array.ts";
import { Format, FormatExtras, PandocFlags } from "../../config/types.ts";
import { createFormat } from "../formats-shared.ts";
@@ -254,6 +257,7 @@ function createPdfFormat(
const partialNamesPandoc: string[] = [
"after-header-includes",
"common",
+ "document-metadata",
"font-settings",
"fonts",
"hypersetup",
@@ -313,6 +317,28 @@ function createPdfFormat(
extras.pandoc[kNumberSections] = true;
}
+ // Handle pdf-standard option for PDF/A, PDF/UA, PDF/X conformance
+ const pdfStandard = asArray(
+ format.render?.[kPdfStandard] ?? format.metadata?.[kPdfStandard],
+ );
+ if (pdfStandard.length > 0) {
+ const { version, standards, needsTagging } =
+ normalizePdfStandardForLatex(pdfStandard);
+ extras.pandoc.variables = extras.pandoc.variables || {};
+ extras.pandoc.variables["pdf-standard"] = true; // Enable the partial
+ if (version) {
+ extras.pandoc.variables["pdf-version"] = version;
+ }
+ if (standards.length > 0) {
+ // Pass as array - Pandoc template will iterate with $for()$
+ extras.pandoc.variables["pdf-standards"] = standards;
+ }
+ // Enable tagging only for standards that require it (ua-*, a-*a)
+ if (needsTagging) {
+ extras.pandoc.variables["pdf-tagging"] = true;
+ }
+ }
+
return extras;
},
},
@@ -1236,3 +1262,75 @@ const kbeginLongTablesideCap = `{
\\makeatother`;
const kEndLongTableSideCap = "}";
+
+// LaTeX-supported PDF standards (from latex3/latex2e DocumentMetadata)
+// See: https://github.com/latex3/latex2e - documentmetadata-support.dtx
+const kLatexSupportedStandards = new Set([
+ // PDF/A standards (note: a-1a is NOT supported, only a-1b)
+ "a-1b",
+ "a-2a",
+ "a-2b",
+ "a-2u",
+ "a-3a",
+ "a-3b",
+ "a-3u",
+ "a-4",
+ "a-4e",
+ "a-4f",
+ // PDF/X standards
+ "x-4",
+ "x-4p",
+ "x-5g",
+ "x-5n",
+ "x-5pg",
+ "x-6",
+ "x-6n",
+ "x-6p",
+ // PDF/UA standards
+ "ua-1",
+ "ua-2",
+]);
+
+// Standards that require PDF tagging (document structure)
+// - PDF/A level "a" variants require tagged structure per PDF/A spec
+// - PDF/UA standards require tagging for universal accessibility
+// (LaTeX does NOT automatically enable tagging for UA standards)
+const kTaggingRequiredStandards = new Set([
+ "a-2a",
+ "a-3a",
+ "ua-1",
+ "ua-2",
+]);
+
+const kVersionPattern = /^(1\.[4-7]|2\.0)$/;
+
+function normalizePdfStandardForLatex(
+ standards: unknown[],
+): { version?: string; standards: string[]; needsTagging: boolean } {
+ let version: string | undefined;
+ const result: string[] = [];
+ let needsTagging = false;
+
+ for (const s of standards) {
+ if (typeof s !== "string") continue;
+ // Normalize: lowercase, remove any "pdf" prefix
+ const normalized = s.toLowerCase().replace(/^pdf[/-]?/, "");
+
+ if (kVersionPattern.test(normalized)) {
+ version = normalized;
+ } else if (kLatexSupportedStandards.has(normalized)) {
+ // LaTeX is case-insensitive, pass through lowercase
+ result.push(normalized);
+ // Check if this standard requires tagging
+ if (kTaggingRequiredStandards.has(normalized)) {
+ needsTagging = true;
+ }
+ } else {
+ warning(
+ `PDF standard '${s}' is not supported by LaTeX and will be ignored`,
+ );
+ }
+ }
+
+ return { version, standards: result, needsTagging };
+}
diff --git a/src/resources/filters/layout/latex.lua b/src/resources/filters/layout/latex.lua
index e5bce66ba4..df5619e8c1 100644
--- a/src/resources/filters/layout/latex.lua
+++ b/src/resources/filters/layout/latex.lua
@@ -332,6 +332,10 @@ function latexCell(cell, vAlign, endOfRow, endOfTable)
-- see if it's a captioned figure
if image and #image.caption > 0 then
caption = image.caption:clone()
+ -- preserve caption as alt attribute for PDF accessibility before clearing
+ if not image.attributes["alt"] then
+ image.attributes["alt"] = pandoc.utils.stringify(image.caption)
+ end
tclear(image.caption)
elseif tbl then
caption = pandoc.utils.blocks_to_inlines(tbl.caption.long)
@@ -380,6 +384,10 @@ function latexCell(cell, vAlign, endOfRow, endOfTable)
if image and #image.caption > 0 then
local caption = image.caption:clone()
markupLatexCaption(cell, caption)
+ -- preserve caption as alt attribute for PDF accessibility before clearing
+ if not image.attributes["alt"] then
+ image.attributes["alt"] = pandoc.utils.stringify(image.caption)
+ end
tclear(image.caption)
content:insert(pandoc.RawBlock("latex", "\\raisebox{-\\height}{"))
content:insert(pandoc.Para(image))
@@ -658,9 +666,13 @@ end
function latexImageFigure(image)
return renderLatexFigure(image, function(figure)
-
+
-- make a copy of the caption and clear it
local caption = image.caption:clone()
+ -- preserve caption as alt attribute for PDF accessibility before clearing
+ if #image.caption > 0 and not image.attributes["alt"] then
+ image.attributes["alt"] = pandoc.utils.stringify(image.caption)
+ end
tclear(image.caption)
-- get align
diff --git a/src/resources/filters/quarto-post/typst.lua b/src/resources/filters/quarto-post/typst.lua
index 6380dc8fee..1c36d20f84 100644
--- a/src/resources/filters/quarto-post/typst.lua
+++ b/src/resources/filters/quarto-post/typst.lua
@@ -94,6 +94,40 @@ function render_typst_fixups()
if image.attributes["width"] ~= nil and type(width_as_number) == "number" then
image.attributes["width"] = tostring(image.attributes["width"] / PANDOC_WRITER_OPTIONS.dpi) .. "in"
end
+
+ -- Workaround for Pandoc not passing alt text to Typst image() calls
+ -- See: https://github.com/jgm/pandoc/issues/XXXX (TODO: file upstream)
+ local alt_text = image.attributes["alt"]
+ if (alt_text == nil or alt_text == "") and #image.caption > 0 then
+ alt_text = pandoc.utils.stringify(image.caption)
+ end
+
+ if alt_text and #alt_text > 0 then
+ -- Build image() parameters
+ local params = {}
+
+ -- Source path (escape backslashes for Windows paths)
+ local src = image.src:gsub('\\', '\\\\')
+ table.insert(params, '"' .. src .. '"')
+
+ -- Alt text second (escape backslashes and quotes)
+ local escaped_alt = alt_text:gsub('\\', '\\\\'):gsub('"', '\\"')
+ table.insert(params, 'alt: "' .. escaped_alt .. '"')
+
+ -- Height if present
+ if image.attributes["height"] then
+ table.insert(params, 'height: ' .. image.attributes["height"])
+ end
+
+ -- Width if present
+ if image.attributes["width"] then
+ table.insert(params, 'width: ' .. image.attributes["width"])
+ end
+
+ -- Use #box() wrapper for inline compatibility
+ return pandoc.RawInline("typst", "#box(image(" .. table.concat(params, ", ") .. "))")
+ end
+
return image
end,
Div = function(div)
diff --git a/src/resources/filters/quarto-pre/figures.lua b/src/resources/filters/quarto-pre/figures.lua
index 20d61f116e..9992644b2f 100644
--- a/src/resources/filters/quarto-pre/figures.lua
+++ b/src/resources/filters/quarto-pre/figures.lua
@@ -38,6 +38,17 @@ end
return float
end
elseif _quarto.format.isLatexOutput() then
+ -- propagate fig-alt to Image elements for LaTeX (enables \includegraphics[alt={...}])
+ local altText = attribute(float, kFigAlt, nil)
+ if altText ~= nil then
+ float.content = _quarto.ast.walk(float.content, {
+ Image = function(image)
+ image.attributes["alt"] = altText
+ return image
+ end
+ })
+ float.attributes[kFigAlt] = nil
+ end
return forward_pos_and_env(float)
end
end,
diff --git a/src/resources/formats/pdf/pandoc/document-metadata.latex b/src/resources/formats/pdf/pandoc/document-metadata.latex
new file mode 100644
index 0000000000..4946202fef
--- /dev/null
+++ b/src/resources/formats/pdf/pandoc/document-metadata.latex
@@ -0,0 +1,13 @@
+$if(pdf-standard)$
+\DocumentMetadata{
+$if(pdf-version)$
+ pdfversion=$pdf-version$,
+$endif$
+$if(pdf-standards)$
+ pdfstandard={$for(pdf-standards)$$it$$sep$,$endfor$},
+$endif$
+$if(pdf-tagging)$
+ tagging=on,
+$endif$
+}
+$endif$
diff --git a/src/resources/formats/pdf/pandoc/template.tex b/src/resources/formats/pdf/pandoc/template.tex
index a2d0943360..5ce3e586c3 100644
--- a/src/resources/formats/pdf/pandoc/template.tex
+++ b/src/resources/formats/pdf/pandoc/template.tex
@@ -1,3 +1,4 @@
+$document-metadata.latex()$
% Options for packages loaded elsewhere
$passoptions.latex()$
%
diff --git a/src/resources/schema/document-pdfa.yml b/src/resources/schema/document-pdfa.yml
index 1faed1f95f..93fac92ac5 100644
--- a/src/resources/schema/document-pdfa.yml
+++ b/src/resources/schema/document-pdfa.yml
@@ -48,3 +48,64 @@
the colors, for example `ISO coated v2 300\letterpercent\space (ECI)`
If left unspecified, `sRGB IEC61966-2.1` is used as default.
+
+- name: pdf-standard
+ schema:
+ maybeArrayOf:
+ enum:
+ # PDF versions
+ - "1.4"
+ - "1.5"
+ - "1.6"
+ - "1.7"
+ - "2.0"
+ # PDF/A standards (supported by both Typst and LaTeX)
+ - a-1b
+ - a-2a
+ - a-2b
+ - a-2u
+ - a-3a
+ - a-3b
+ - a-3u
+ - a-4
+ - a-4f
+ # PDF/A standards (Typst only)
+ - a-1a
+ - a-4e
+ # PDF/UA standards
+ - ua-1
+ - ua-2
+ # PDF/X standards (LaTeX only)
+ - x-4
+ - x-4p
+ - x-5g
+ - x-5n
+ - x-5pg
+ - x-6
+ - x-6n
+ - x-6p
+ tags:
+ formats: [$pdf-all, typst]
+ description:
+ short: PDF conformance standard (e.g., a-2b, ua-1, 1.7)
+ long: |
+ Specifies PDF conformance standards and/or version for the output.
+
+ Accepts a single value or array of values:
+
+ **PDF versions** (both Typst and LaTeX):
+ `1.4`, `1.5`, `1.6`, `1.7`, `2.0`
+
+ **PDF/A standards** (both engines):
+ `a-1b`, `a-2a`, `a-2b`, `a-2u`, `a-3a`, `a-3b`, `a-3u`, `a-4`, `a-4f`
+
+ **PDF/A standards** (Typst only):
+ `a-1a`, `a-4e`
+
+ **PDF/UA standards**:
+ `ua-1` (both), `ua-2` (LaTeX only)
+
+ **PDF/X standards** (LaTeX only):
+ `x-4`, `x-4p`, `x-5g`, `x-5n`, `x-5pg`, `x-6`, `x-6n`, `x-6p`
+
+ Example: `pdf-standard: [a-2b, ua-1]` for accessible archival PDF.
diff --git a/tests/docs/smoke-all/pdf-standard/alt-test.qmd b/tests/docs/smoke-all/pdf-standard/alt-test.qmd
new file mode 100644
index 0000000000..0a1b35f669
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/alt-test.qmd
@@ -0,0 +1,8 @@
+---
+title: "Alt text test"
+pdf-standard: ua-1
+keep-tex: true
+---
+
+
+
diff --git a/tests/docs/smoke-all/pdf-standard/latex-combined.qmd b/tests/docs/smoke-all/pdf-standard/latex-combined.qmd
new file mode 100644
index 0000000000..9a7f139410
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/latex-combined.qmd
@@ -0,0 +1,17 @@
+---
+title: "LaTeX version + standards combined"
+format: pdf
+pdf-standard: ["1.7", a-2b, ua-1]
+keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ - ['\\DocumentMetadata\{', 'pdfversion=1\.7', 'pdfstandard=\{a-2b,ua-1\}', 'tagging=on']
+ - []
+---
+
+# Test Document
+
+This tests PDF 1.7 with PDF/A-2b + PDF/UA-1.
diff --git a/tests/docs/smoke-all/pdf-standard/latex-multi-standard.qmd b/tests/docs/smoke-all/pdf-standard/latex-multi-standard.qmd
new file mode 100644
index 0000000000..ad76943c42
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/latex-multi-standard.qmd
@@ -0,0 +1,17 @@
+---
+title: "LaTeX multiple PDF standards"
+format: pdf
+pdf-standard: [a-2b, ua-1]
+keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ - ['\\DocumentMetadata\{', 'pdfstandard=\{a-2b,ua-1\}', 'tagging=on']
+ - []
+---
+
+# Test Document
+
+This tests combined PDF/A-2b + PDF/UA-1 output.
diff --git a/tests/docs/smoke-all/pdf-standard/latex-pdfa-accessible.qmd b/tests/docs/smoke-all/pdf-standard/latex-pdfa-accessible.qmd
new file mode 100644
index 0000000000..cc891b98df
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/latex-pdfa-accessible.qmd
@@ -0,0 +1,18 @@
+---
+title: "LaTeX PDF/A-2a standard (accessible)"
+format: pdf
+pdf-standard: a-2a
+keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ # a-2a is "accessible" level - REQUIRES tagging
+ - ['\\DocumentMetadata\{', 'pdfstandard=\{a-2a\}', 'tagging=on']
+ - []
+---
+
+# Test Document
+
+This tests PDF/A-2a output with LaTeX. The "a" (accessible) level requires tagging.
diff --git a/tests/docs/smoke-all/pdf-standard/latex-pdfa.qmd b/tests/docs/smoke-all/pdf-standard/latex-pdfa.qmd
new file mode 100644
index 0000000000..87551781f4
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/latex-pdfa.qmd
@@ -0,0 +1,18 @@
+---
+title: "LaTeX PDF/A-2b standard"
+format: pdf
+pdf-standard: a-2b
+keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ # a-2b is "basic" level - does NOT require tagging
+ - ['\\DocumentMetadata\{', 'pdfstandard=\{a-2b\}']
+ - ['tagging=on']
+---
+
+# Test Document
+
+This tests PDF/A-2b output with LaTeX. The "b" (basic) level does not require tagging.
diff --git a/tests/docs/smoke-all/pdf-standard/latex-pdfversion.qmd b/tests/docs/smoke-all/pdf-standard/latex-pdfversion.qmd
new file mode 100644
index 0000000000..4e0ba58d8f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/latex-pdfversion.qmd
@@ -0,0 +1,18 @@
+---
+title: "LaTeX PDF version"
+format: pdf
+pdf-standard: "1.7"
+keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ # Version-only should NOT enable tagging (tagging is independent of version)
+ - ['\\DocumentMetadata\{', 'pdfversion=1\.7']
+ - ['tagging=on']
+---
+
+# Test Document
+
+This tests explicit PDF version 1.7 without any standard requiring tagging.
diff --git a/tests/docs/smoke-all/pdf-standard/latex-unsupported-warning.qmd b/tests/docs/smoke-all/pdf-standard/latex-unsupported-warning.qmd
new file mode 100644
index 0000000000..3e24f2650d
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/latex-unsupported-warning.qmd
@@ -0,0 +1,17 @@
+---
+title: "LaTeX unsupported standard warning"
+format: pdf
+pdf-standard: a-1a
+keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ printsMessage:
+ level: WARN
+ regex: "PDF standard 'a-1a' is not supported by LaTeX"
+---
+
+# Test Document
+
+This tests that PDF/A-1a (Typst-only) produces a warning with LaTeX.
diff --git a/tests/docs/smoke-all/pdf-standard/penrose.svg b/tests/docs/smoke-all/pdf-standard/penrose.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/penrose.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/tc1-figure.svg b/tests/docs/smoke-all/pdf-standard/tc1-figure.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/tc1-figure.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/tc2-inline.svg b/tests/docs/smoke-all/pdf-standard/tc2-inline.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/tc2-inline.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/tc3-explicit.svg b/tests/docs/smoke-all/pdf-standard/tc3-explicit.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/tc3-explicit.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/tc4-dimensions.svg b/tests/docs/smoke-all/pdf-standard/tc4-dimensions.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/tc4-dimensions.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/tc5-quotes.svg b/tests/docs/smoke-all/pdf-standard/tc5-quotes.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/tc5-quotes.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/tc6-backslash.svg b/tests/docs/smoke-all/pdf-standard/tc6-backslash.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/tc6-backslash.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/tc7-no-alt.svg b/tests/docs/smoke-all/pdf-standard/tc7-no-alt.svg
new file mode 100644
index 0000000000..30308f170f
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/tc7-no-alt.svg
@@ -0,0 +1,16 @@
+
diff --git a/tests/docs/smoke-all/pdf-standard/typst-image-alt-text.qmd b/tests/docs/smoke-all/pdf-standard/typst-image-alt-text.qmd
new file mode 100644
index 0000000000..c9070ce0e8
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/typst-image-alt-text.qmd
@@ -0,0 +1,57 @@
+---
+title: "Typst image alt text support"
+format:
+ typst:
+ keep-typ: true
+_quarto:
+ tests:
+ typst:
+ noErrors: default
+ ensureTypstFileRegexMatches:
+ - # Patterns that MUST be found - alt text in image() calls
+ - 'image\("tc1-figure\.svg",\s*alt:\s*"TC1 figure caption as alt'
+ - 'image\("tc2-inline\.svg",\s*alt:\s*"TC2 inline image'
+ - 'image\("tc3-explicit\.svg",\s*alt:\s*"TC3 explicit alt attribute'
+ - 'image\("tc4-dimensions\.svg",\s*alt:\s*"TC4 with dimensions",\s*height:\s*1in,\s*width:\s*1in'
+ - 'image\("tc5-quotes\.svg",\s*alt:\s*"TC5 with \\"escaped\\" quotes'
+ - 'image\("tc6-backslash\.svg",\s*alt:\s*"TC6 backslash C:\\\\path'
+ # TC7 should have the image but without alt parameter
+ - 'image\("tc7-no-alt\.svg"\)'
+ - # Patterns that must NOT be found
+ # TC7 with no caption/alt should NOT have alt parameter
+ - 'tc7-no-alt\.svg.*alt:'
+---
+
+# Test Document: Typst Image Alt Text
+
+This tests that Quarto passes alt text to Typst's `image()` function for PDF/UA accessibility.
+
+## TC1: Figure with alt text from caption
+
+
+
+## TC2: Inline image with alt text
+
+Here is an icon  in the text.
+
+## TC3: Explicit alt attribute (different from caption)
+
+{alt="TC3 explicit alt attribute"}
+
+## TC4: Image with dimensions should preserve alt
+
+Here is {width=1in height=1in} inline.
+
+## TC5: Alt text with special characters (quotes)
+
+{alt="TC5 with \"escaped\" quotes"}
+
+## TC6: Alt text with backslashes
+
+{alt="TC6 backslash C:\\path\\file"}
+
+## TC7: Image with no alt text (should omit alt parameter)
+
+This image has no caption and no alt attribute.
+
+
diff --git a/tests/docs/smoke-all/pdf-standard/typst-pdfa.qmd b/tests/docs/smoke-all/pdf-standard/typst-pdfa.qmd
new file mode 100644
index 0000000000..52c42d01e2
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/typst-pdfa.qmd
@@ -0,0 +1,14 @@
+---
+title: "Typst PDF/A-2b standard"
+format: typst
+pdf-standard: a-2b
+keep-typ: true
+_quarto:
+ tests:
+ typst:
+ noErrors: default
+---
+
+# Test Document
+
+This tests PDF/A-2b output with Typst.
diff --git a/tests/docs/smoke-all/pdf-standard/typst-unsupported-warning.qmd b/tests/docs/smoke-all/pdf-standard/typst-unsupported-warning.qmd
new file mode 100644
index 0000000000..74f2e72132
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/typst-unsupported-warning.qmd
@@ -0,0 +1,17 @@
+---
+title: "Typst unsupported standard warning"
+format: typst
+pdf-standard: x-4
+keep-typ: true
+_quarto:
+ tests:
+ typst:
+ noErrors: default
+ printsMessage:
+ level: WARN
+ regex: "PDF standard 'x-4' is not supported by Typst"
+---
+
+# Test Document
+
+This tests that PDF/X-4 (LaTeX-only) produces a warning with Typst.
diff --git a/tests/docs/smoke-all/pdf-standard/typst-version-and-standard.qmd b/tests/docs/smoke-all/pdf-standard/typst-version-and-standard.qmd
new file mode 100644
index 0000000000..6a569e372d
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/typst-version-and-standard.qmd
@@ -0,0 +1,14 @@
+---
+title: "Typst PDF version + standard"
+format: typst
+pdf-standard: ["1.7", a-2b]
+keep-typ: true
+_quarto:
+ tests:
+ typst:
+ noErrors: default
+---
+
+# Test Document
+
+This tests combined PDF 1.7 + PDF/A-2b output with Typst.
diff --git a/tests/docs/smoke-all/pdf-standard/ua1-compliant.qmd b/tests/docs/smoke-all/pdf-standard/ua1-compliant.qmd
new file mode 100644
index 0000000000..33519da149
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/ua1-compliant.qmd
@@ -0,0 +1,24 @@
+---
+title: "UA-1 with language set"
+lang: en
+pdf-standard: ua-1
+keep-tex: true
+keep-typ: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ - ['\\DocumentMetadata\{', 'pdfstandard=\{ua-1\}', 'tagging=on']
+ - []
+ printsMessage:
+ level: WARN
+ regex: "Document language not set"
+ negate: true
+ typst:
+ noErrors: default
+---
+
+# Test Document
+
+This document has UA-1 with language properly set, so no language warning should appear.
diff --git a/tests/docs/smoke-all/pdf-standard/ua1-image-alt-text.qmd b/tests/docs/smoke-all/pdf-standard/ua1-image-alt-text.qmd
new file mode 100644
index 0000000000..f48bfbfb1b
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/ua1-image-alt-text.qmd
@@ -0,0 +1,25 @@
+---
+title: "UA-1 image with alt text"
+lang: en
+pdf-standard: ua-1
+keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ # Alt text MUST be passed to \includegraphics[alt={...}] for PDF/UA
+ - ['\\DocumentMetadata\{', 'pdfstandard=\{ua-1\}', 'tagging=on', 'includegraphics\[.*alt=']
+ - []
+ printsMessage:
+ # Should NOT warn about missing alt text since we provided it
+ level: WARN
+ regex: "PDF accessibility:.*Missing alt text"
+ negate: true
+---
+
+# Test Document
+
+This image has alt text which should be passed through to LaTeX's `\includegraphics[alt={...}]` for PDF/UA compliance.
+
+
diff --git a/tests/docs/smoke-all/pdf-standard/ua1-missing-alt.qmd b/tests/docs/smoke-all/pdf-standard/ua1-missing-alt.qmd
new file mode 100644
index 0000000000..1638d70f33
--- /dev/null
+++ b/tests/docs/smoke-all/pdf-standard/ua1-missing-alt.qmd
@@ -0,0 +1,32 @@
+---
+title: "UA-1 missing alt text"
+pdf-standard: ua-1
+keep-tex: true
+keep-typ: true
+_quarto:
+ tests:
+ pdf:
+ noErrors: default
+ ensureLatexFileRegexMatches:
+ - ['\\DocumentMetadata\{', 'pdfstandard=\{ua-1\}', 'tagging=on']
+ - []
+ printsMessage:
+ level: WARN
+ regex: "PDF accessibility:"
+ typst:
+ shouldError: default
+---
+
+# Test Document
+
+This image has no alt text. Typst should fail UA-1 validation; LaTeX should warn.
+
+
+
+