Skip to content

Commit c54f70e

Browse files
Replace fig-pos [H] with [h] when PDF tagging is active
The [H] placement specifier (from LaTeX's float package) breaks lualatex's PDF tag structure, causing /Caption and /Figure to appear as direct children of /Document instead of being properly nested. Standard [h] ("place here") is the closest equivalent without the float package's incompatible reimplementation. When pdf-standard requires tagging (e.g. ua-2), all automatic [H] injections are replaced with [h] across the filter pipeline: - quarto-pre/figures.lua (execution engine fig-pos defaults) - quarto-post/latex.lua (panel layouts and star environments) - layout/pandoc3_figure.lua (unlabeled figures) Fixes #14164
1 parent 20cb9d2 commit c54f70e

6 files changed

Lines changed: 73 additions & 33 deletions

File tree

src/format/pdf/format-pdf.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,12 @@ function createPdfFormat(
352352
extras.metadata = extras.metadata || {};
353353
extras.metadata[kPdfStandardApplied] = standards;
354354
}
355+
// Expose tagging state to Lua filters so they can adjust
356+
// figure placement (e.g., avoid [H] which breaks tag structure)
357+
if (needsTagging) {
358+
extras.metadata = extras.metadata || {};
359+
extras.metadata["pdf-tagging"] = true;
360+
}
355361
}
356362

357363
return extras;

src/resources/filters/layout/pandoc3_figure.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,13 @@ function render_pandoc3_figure()
124124
-- if this ends up in a layout without fig-pos = H, it'll fail
125125
-- 'H' forces it to not float
126126
if figure.identifier == "" then
127+
-- Use [htbp] instead of [H] when PDF tagging is active.
128+
-- [H] (float package) breaks lualatex's tag structure.
129+
-- See https://github.com/quarto-dev/quarto-cli/issues/14164
130+
local forced_pos = option("pdf-tagging", false) and "h" or "H"
127131
figure = _quarto.ast.walk(figure, {
128132
Image = function(image)
129-
image.attributes['fig-pos'] = 'H'
133+
image.attributes['fig-pos'] = forced_pos
130134
return image
131135
end
132136
})

src/resources/filters/quarto-post/latex.lua

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,14 +260,19 @@ function render_latex()
260260
if f ~= nil then
261261
noteHasColumns()
262262
el.content = strip(el.content, f)
263-
tprepend(el.content, {pandoc.RawBlock("latex", "\\begin{figure*}[H]")})
263+
local star_pos = option("pdf-tagging", false) and "h" or "H"
264+
tprepend(el.content, {pandoc.RawBlock("latex", "\\begin{figure*}[" .. star_pos .. "]")})
264265
tappend(el.content, {pandoc.RawBlock("latex", "\\end{figure*}")})
265266
return el, false
266267
end
267268
end
268269
end
269270

270271
local function handle_panel_layout(panel)
272+
-- Use [htbp] instead of [H] when PDF tagging is active.
273+
-- [H] (float package) breaks lualatex's tag structure.
274+
-- See https://github.com/quarto-dev/quarto-cli/issues/14164
275+
local forced_pos = option("pdf-tagging", false) and "h" or "H"
271276
panel.rows = _quarto.ast.walk(panel.rows, {
272277
FloatRefTarget = function(float)
273278
if float.attributes["ref-parent"] == nil then
@@ -278,14 +283,14 @@ function render_latex()
278283
-- give up
279284
return nil
280285
end
281-
float.attributes[ref .. "-pos"] = "H"
286+
float.attributes[ref .. "-pos"] = forced_pos
282287
return float
283288
end
284289
end,
285290
Figure = function(figure)
286291
if figure.identifier ~= nil then
287292
local ref = refType(figure.identifier) or "fig"
288-
figure.attributes[ref .. "-pos"] = "H"
293+
figure.attributes[ref .. "-pos"] = forced_pos
289294
end
290295
return figure
291296
end
@@ -337,11 +342,12 @@ function render_latex()
337342
end
338343
})
339344
end
345+
local panel_pos = option("pdf-tagging", false) and "h" or "H"
340346
float.content = _quarto.ast.walk(quarto.utils.as_blocks(float.content), {
341347
PanelLayout = function(panel)
342-
panel.attributes["fig-pos"] = "H"
348+
panel.attributes["fig-pos"] = panel_pos
343349
return panel
344-
end
350+
end
345351
})
346352
return float, false
347353
end,

src/resources/filters/quarto-pre/figures.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ function quarto_pre_figures()
1414
if el.attributes[kFigPos] == "FALSE" then
1515
el.attributes[kFigPos] = nil
1616
end
17+
-- Replace fig-pos='H' with 'htbp' when PDF tagging is active.
18+
-- The [H] specifier (float package) breaks lualatex's tag structure,
19+
-- causing /Caption and /Figure to be direct children of /Document.
20+
-- Standard [htbp] works correctly with tagging.
21+
-- See https://github.com/quarto-dev/quarto-cli/issues/14164
22+
if el.attributes[kFigPos] == "H" and option("pdf-tagging", false) then
23+
el.attributes[kFigPos] = "h"
24+
end
1725
local figEnv = param(kFigEnv)
1826

1927
if figEnv and not el.attributes[kFigEnv] then
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: "Code chunk figure tag structure"
3+
lang: en
4+
format:
5+
pdf:
6+
pdf-standard: ua-2
7+
keep-tex: true
8+
_quarto:
9+
tests:
10+
run:
11+
# verapdf validation not available on Windows CI
12+
not_os: windows
13+
pdf:
14+
noErrors: default
15+
ensureLatexFileRegexMatches:
16+
- ['\\DocumentMetadata\{', 'pdfstandard=\{ua-2\}', 'tagging=on', '\\begin\{figure\}']
17+
# Must NOT have \begin{figure}[H] — [H] breaks PDF tag structure
18+
# See https://github.com/quarto-dev/quarto-cli/issues/14164
19+
- ['\\begin\{figure\}\[H\]']
20+
---
21+
22+
# Code chunk figure with cross-ref
23+
24+
R code chunk figures with `fig-cap` get automatic `fig-pos='H'` when code is echoed.
25+
The `[H]` specifier (from LaTeX's `float` package) breaks lualatex's PDF tagging,
26+
causing `/Caption` and `/Figure` to be direct children of `/Document` instead of
27+
nested inside `/figures``/float`.
28+
29+
```{r}
30+
#| label: fig-test-plot
31+
#| fig-cap: "Test plot"
32+
plot(1:10)
33+
```
34+
35+
See @fig-test-plot.
Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: "UA-2: unlabeled figure caption produces invalid structure"
2+
title: "UA-2: unlabeled figure caption"
33
lang: en
44
format:
55
pdf:
@@ -14,34 +14,15 @@ _quarto:
1414
noErrors: default
1515
ensureLatexFileRegexMatches:
1616
- ['\\DocumentMetadata\{', 'pdfstandard=\{ua-2\}', 'tagging=on']
17-
- []
18-
printsMessage:
19-
# Known issue: unlabeled captioned figures go through pandoc3_figure.lua
20-
# which produces a bare \begin{figure}[H] environment. LaTeX's tagpdf
21-
# places <Caption> as a sibling of <Figure> under <Document> instead of
22-
# nesting them inside a grouping element. This violates UA-2 which
23-
# requires <Caption> to be a child of <Figure>, <Table>, or <Formula>.
24-
#
25-
# Labeled figures ({#fig-label}) go through FloatRefTarget, which wraps
26-
# the figure in a \Div that provides the grouping context tagpdf needs.
27-
#
28-
# Structure produced (invalid):
29-
# /Document
30-
# /Caption <- should be inside a grouping element
31-
# /Figure <- sibling instead of parent
32-
#
33-
# Expected (valid, as produced by labeled figures):
34-
# /Document
35-
# /Div
36-
# /Caption <- properly nested
37-
# /Figure
38-
level: WARN
39-
regex: "PDF validation failed for ua-2"
17+
# Must NOT have \begin{figure}[H] — [H] breaks PDF tag structure
18+
# See https://github.com/quarto-dev/quarto-cli/issues/14164
19+
- ['\\begin\{figure\}\[H\]']
4020
---
4121

42-
# Known LaTeX tagging limitation
22+
# Unlabeled captioned figure
4323

44-
An unlabeled captioned figure produces invalid UA-2 structure because
45-
tagpdf does not nest `<Caption>` inside a grouping element.
24+
An unlabeled captioned figure. Previously this produced invalid UA-2
25+
structure because `[H]` placement broke lualatex's tag tree. With
26+
`[htbp]` the tag structure is correct.
4627

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

0 commit comments

Comments
 (0)