Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/command/render/latexmk/parse-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,10 @@ export function findPdfAccessibilityWarnings(

// Match: Package tagpdf Warning: Alternative text for graphic is missing.
// (tagpdf) Using 'filename' instead.
// Note: tagpdf wraps long filenames across multiple (tagpdf) continuation
// lines, so we allow optional line breaks with (tagpdf) prefixes.
const altTextRegex =
/Package tagpdf Warning: Alternative text for graphic is missing\.\s*\n\(tagpdf\)\s*Using ['`]([^'`]+)['`] instead\./g;
/Package tagpdf Warning: Alternative text for graphic is missing\.\s*\n\(tagpdf\)\s*Using ['`]([^'`]+)['`]\s*(?:\n\(tagpdf\)\s*)?instead\./g;
let match;
while ((match = altTextRegex.exec(logText)) !== null) {
result.missingAltText.push(match[1]);
Expand Down
2 changes: 1 addition & 1 deletion src/command/render/latexmk/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ async function initialCompileLatex(
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`,
`PDF accessibility: Missing alt text for image(s): ${fileList}. Add alt text using fig-alt in YAML or {fig-alt="description"} on the image for PDF/UA compliance.\n`,
);
}
if (accessibilityWarnings.missingLanguage) {
Expand Down
16 changes: 16 additions & 0 deletions src/resources/filters/customnodes/floatreftarget.lua
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,22 @@ end, function(float)
local kind = "quarto-float-" .. ref
local supplement = titleString(ref, info.name)

-- For figures: mark images so typst.lua won't use caption-as-alt fallback
-- when caption IS the visible figure caption (not an explicit alt override).
-- In Pandoc 3, {alt="text"} replaces image.caption with the alt value,
-- so image.caption != float.caption means an explicit alt was provided.
if ref == "fig" then
local float_caption_text = pandoc.utils.stringify(float.caption_long or {})
float.content = _quarto.ast.walk(float.content, {
Image = function(img)
if pandoc.utils.stringify(img.caption) == float_caption_text then
img.attributes["_quarto_no_caption_alt"] = "true"
end
return img
end
})
end

-- Inject show rule to left-align listing figures (only once per document)
-- This overrides any template centering for listing-kind figures
-- https://github.com/quarto-dev/quarto-cli/issues/9724
Expand Down
12 changes: 0 additions & 12 deletions src/resources/filters/layout/latex.lua
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,6 @@ 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)
Expand Down Expand Up @@ -384,10 +380,6 @@ 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))
Expand Down Expand Up @@ -669,10 +661,6 @@ function latexImageFigure(image)

-- 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
Expand Down
27 changes: 27 additions & 0 deletions src/resources/filters/layout/pandoc3_figure.lua
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ function render_pandoc3_figure()
for k, v in pairs(figure.attributes) do
image.attributes[k] = v
end
-- Convert fig-alt to alt for LaTeX \includegraphics[alt=...]
if image.attributes[kFigAlt] then
if not image.attributes["alt"] then
image.attributes["alt"] = image.attributes[kFigAlt]
end
image.attributes[kFigAlt] = nil
end
if subfig then
image.attributes['quarto-caption-env'] = 'subcaption'
end
Expand All @@ -170,6 +177,26 @@ function render_pandoc3_figure()
return {
traverse = "topdown",
Figure = function(figure)
-- For figure images: prevent caption-as-alt fallback when caption IS the
-- visible figure caption (not an explicit alt override via {alt="..."}).
-- In Pandoc 3, {alt="text"} replaces image.caption with the alt value,
-- so image.caption != figure.caption means an explicit alt was provided.
-- Also propagate fig-alt from figure to image for accessibility.
local figure_caption_text = pandoc.utils.stringify(figure.caption.long)
local fig_alt = figure.attributes[kFigAlt]
for _, block in ipairs(figure.content) do
if block.t == "Plain" or block.t == "Para" then
for _, inline in ipairs(block.content) do
if inline.t == "Image" then
if fig_alt then
inline.attributes[kFigAlt] = fig_alt
elseif pandoc.utils.stringify(inline.caption) == figure_caption_text then
inline.attributes["_quarto_no_caption_alt"] = "true"
end
end
end
end
end
return make_typst_figure({
content = figure.content[1],
caption = figure.caption.long[1],
Expand Down
6 changes: 5 additions & 1 deletion src/resources/filters/quarto-post/typst.lua
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,11 @@ function render_typst_fixups()
if alt_text then
image.attributes[kFigAlt] = nil
end
if (alt_text == nil or alt_text == "") and #image.caption > 0 then
-- Use caption as alt only for inline images (not figures)
-- Figure images are marked with _quarto_no_caption_alt by layout filters
local no_caption_alt = image.attributes["_quarto_no_caption_alt"]
image.attributes["_quarto_no_caption_alt"] = nil
if (alt_text == nil or alt_text == "") and #image.caption > 0 and not no_caption_alt then
alt_text = pandoc.utils.stringify(image.caption)
end

Expand Down
41 changes: 41 additions & 0 deletions tests/docs/smoke-all/pdf-standard/caption-not-alt-ua.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: "Caption should not be used as fallback alt text"
lang: en
format:
pdf:
pdf-standard: ua-2
keep-tex: true
typst:
pdf-standard: ua-1
keep-typ: true
_quarto:
tests:
run:
# verapdf validation not available on Windows CI
not_os: windows
pdf:
noErrors: default
ensureLatexFileRegexMatches:
- ['\\DocumentMetadata\{', 'pdfstandard=\{ua-2\}', 'tagging=on']
- # Caption must NOT appear as alt text on \includegraphics
['includegraphics\[.*alt=']
printsMessage:
# tagpdf warns about missing alt text and falls back to filename;
# Quarto surfaces this as a user-facing warning
level: WARN
regex: "PDF accessibility:.*Missing alt text"
typst:
# Typst's own PDF/UA enforcement errors on missing alt
shouldError: default
---

# Caption is not alt text

A figure with a caption but no explicit `fig-alt`.
The caption should NOT be copied into alt text.

Uses a cross-ref label to go through FloatRefTarget, which produces
valid UA-2 structure. See `ua2-unlabeled-figure-caption.qmd` for the
known structural issue with unlabeled captioned figures.

![This is a caption, not alt text](penrose.svg){#fig-test}
16 changes: 16 additions & 0 deletions tests/docs/smoke-all/pdf-standard/tc8-fig-alt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion tests/docs/smoke-all/pdf-standard/typst-image-alt-text.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ _quarto:
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"\)'
# TC8: Explicit fig-alt should produce alt text
- 'image\("tc8-fig-alt\.svg",\s*alt:\s*"TC8 explicit fig-alt'
- # Patterns that must NOT be found
# TC1 figure caption should NOT be used as alt text
- 'tc1-figure\.svg.*alt:.*TC1 figure caption'
# TC7 with no caption/alt should NOT have alt parameter
- 'tc7-no-alt\.svg.*alt:'
---
Expand Down Expand Up @@ -55,3 +58,7 @@ Here is ![TC4 with dimensions](tc4-dimensions.svg){width=1in height=1in} inline.
This image has no caption and no alt attribute.

![](tc7-no-alt.svg)

## TC8: Explicit fig-alt on a figure

![TC8 visible caption](tc8-fig-alt.svg){fig-alt="TC8 explicit fig-alt description"}
2 changes: 1 addition & 1 deletion tests/docs/smoke-all/pdf-standard/ua-image-alt-text.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ _quarto:

This image has alt text which should be passed through for PDF/UA compliance.

![Test image description](penrose.svg)
![Test image description](penrose.svg){fig-alt="A Penrose tiling pattern"}
47 changes: 47 additions & 0 deletions tests/docs/smoke-all/pdf-standard/ua2-unlabeled-figure-caption.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: "UA-2: unlabeled figure caption produces invalid structure"
lang: en
format:
pdf:
pdf-standard: ua-2
keep-tex: true
_quarto:
tests:
run:
# verapdf validation not available on Windows CI
not_os: windows
pdf:
noErrors: default
ensureLatexFileRegexMatches:
- ['\\DocumentMetadata\{', 'pdfstandard=\{ua-2\}', 'tagging=on']
- []
printsMessage:
# Known issue: unlabeled captioned figures go through pandoc3_figure.lua
# which produces a bare \begin{figure}[H] environment. LaTeX's tagpdf
# places <Caption> as a sibling of <Figure> under <Document> instead of
# nesting them inside a grouping element. This violates UA-2 which
# requires <Caption> to be a child of <Figure>, <Table>, or <Formula>.
#
# Labeled figures ({#fig-label}) go through FloatRefTarget, which wraps
# the figure in a \Div that provides the grouping context tagpdf needs.
#
# Structure produced (invalid):
# /Document
# /Caption <- should be inside a grouping element
# /Figure <- sibling instead of parent
#
# Expected (valid, as produced by labeled figures):
# /Document
# /Div
# /Caption <- properly nested
# /Figure
level: WARN
regex: "PDF validation failed for ua-2"
---

# Known LaTeX tagging limitation

An unlabeled captioned figure produces invalid UA-2 structure because
tagpdf does not nest `<Caption>` inside a grouping element.

![This is a caption on an unlabeled figure](penrose.svg)
Loading