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 ![alt text](image.png) 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 +--- + +![This is the alt text](penrose.svg) + 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 + +![TC1 figure caption as alt text](tc1-figure.svg) + +## TC2: Inline image with alt text + +Here is an icon ![TC2 inline image](tc2-inline.svg) in the text. + +## TC3: Explicit alt attribute (different from caption) + +![Visible caption](tc3-explicit.svg){alt="TC3 explicit alt attribute"} + +## TC4: Image with dimensions should preserve alt + +Here is ![TC4 with dimensions](tc4-dimensions.svg){width=1in height=1in} inline. + +## TC5: Alt text with special characters (quotes) + +![Caption](tc5-quotes.svg){alt="TC5 with \"escaped\" quotes"} + +## TC6: Alt text with backslashes + +![Caption](tc6-backslash.svg){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. + +![](tc7-no-alt.svg) 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. + +![Test image description](penrose.svg) 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. + + + +![](penrose.svg)