From c54f70ea3f7a2e06bbbc0727560b03b1cf8df3ba Mon Sep 17 00:00:00 2001 From: Gordon Woodhull Date: Thu, 5 Mar 2026 23:10:44 -0500 Subject: [PATCH] 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 --- src/format/pdf/format-pdf.ts | 6 ++++ .../filters/layout/pandoc3_figure.lua | 6 +++- src/resources/filters/quarto-post/latex.lua | 16 ++++++--- src/resources/filters/quarto-pre/figures.lua | 8 +++++ .../pdf-standard/ua-code-chunk-figure.qmd | 35 +++++++++++++++++++ .../ua2-unlabeled-figure-caption.qmd | 35 +++++-------------- 6 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 tests/docs/smoke-all/pdf-standard/ua-code-chunk-figure.qmd diff --git a/src/format/pdf/format-pdf.ts b/src/format/pdf/format-pdf.ts index b38ada6b204..c1bf5bfead7 100644 --- a/src/format/pdf/format-pdf.ts +++ b/src/format/pdf/format-pdf.ts @@ -352,6 +352,12 @@ function createPdfFormat( extras.metadata = extras.metadata || {}; extras.metadata[kPdfStandardApplied] = standards; } + // Expose tagging state to Lua filters so they can adjust + // figure placement (e.g., avoid [H] which breaks tag structure) + if (needsTagging) { + extras.metadata = extras.metadata || {}; + extras.metadata["pdf-tagging"] = true; + } } return extras; diff --git a/src/resources/filters/layout/pandoc3_figure.lua b/src/resources/filters/layout/pandoc3_figure.lua index 42b66e54f28..9b05d0ef4bc 100644 --- a/src/resources/filters/layout/pandoc3_figure.lua +++ b/src/resources/filters/layout/pandoc3_figure.lua @@ -124,9 +124,13 @@ function render_pandoc3_figure() -- if this ends up in a layout without fig-pos = H, it'll fail -- 'H' forces it to not float if figure.identifier == "" then + -- Use [htbp] instead of [H] when PDF tagging is active. + -- [H] (float package) breaks lualatex's tag structure. + -- See https://github.com/quarto-dev/quarto-cli/issues/14164 + local forced_pos = option("pdf-tagging", false) and "h" or "H" figure = _quarto.ast.walk(figure, { Image = function(image) - image.attributes['fig-pos'] = 'H' + image.attributes['fig-pos'] = forced_pos return image end }) diff --git a/src/resources/filters/quarto-post/latex.lua b/src/resources/filters/quarto-post/latex.lua index 11f18f296fe..b46494f9158 100644 --- a/src/resources/filters/quarto-post/latex.lua +++ b/src/resources/filters/quarto-post/latex.lua @@ -260,7 +260,8 @@ function render_latex() if f ~= nil then noteHasColumns() el.content = strip(el.content, f) - tprepend(el.content, {pandoc.RawBlock("latex", "\\begin{figure*}[H]")}) + local star_pos = option("pdf-tagging", false) and "h" or "H" + tprepend(el.content, {pandoc.RawBlock("latex", "\\begin{figure*}[" .. star_pos .. "]")}) tappend(el.content, {pandoc.RawBlock("latex", "\\end{figure*}")}) return el, false end @@ -268,6 +269,10 @@ function render_latex() end local function handle_panel_layout(panel) + -- Use [htbp] instead of [H] when PDF tagging is active. + -- [H] (float package) breaks lualatex's tag structure. + -- See https://github.com/quarto-dev/quarto-cli/issues/14164 + local forced_pos = option("pdf-tagging", false) and "h" or "H" panel.rows = _quarto.ast.walk(panel.rows, { FloatRefTarget = function(float) if float.attributes["ref-parent"] == nil then @@ -278,14 +283,14 @@ function render_latex() -- give up return nil end - float.attributes[ref .. "-pos"] = "H" + float.attributes[ref .. "-pos"] = forced_pos return float end end, Figure = function(figure) if figure.identifier ~= nil then local ref = refType(figure.identifier) or "fig" - figure.attributes[ref .. "-pos"] = "H" + figure.attributes[ref .. "-pos"] = forced_pos end return figure end @@ -337,11 +342,12 @@ function render_latex() end }) end + local panel_pos = option("pdf-tagging", false) and "h" or "H" float.content = _quarto.ast.walk(quarto.utils.as_blocks(float.content), { PanelLayout = function(panel) - panel.attributes["fig-pos"] = "H" + panel.attributes["fig-pos"] = panel_pos return panel - end + end }) return float, false end, diff --git a/src/resources/filters/quarto-pre/figures.lua b/src/resources/filters/quarto-pre/figures.lua index cd474a0ada0..0b18aad93f4 100644 --- a/src/resources/filters/quarto-pre/figures.lua +++ b/src/resources/filters/quarto-pre/figures.lua @@ -14,6 +14,14 @@ function quarto_pre_figures() if el.attributes[kFigPos] == "FALSE" then el.attributes[kFigPos] = nil end + -- Replace fig-pos='H' with 'htbp' when PDF tagging is active. + -- The [H] specifier (float package) breaks lualatex's tag structure, + -- causing /Caption and /Figure to be direct children of /Document. + -- Standard [htbp] works correctly with tagging. + -- See https://github.com/quarto-dev/quarto-cli/issues/14164 + if el.attributes[kFigPos] == "H" and option("pdf-tagging", false) then + el.attributes[kFigPos] = "h" + end local figEnv = param(kFigEnv) if figEnv and not el.attributes[kFigEnv] then diff --git a/tests/docs/smoke-all/pdf-standard/ua-code-chunk-figure.qmd b/tests/docs/smoke-all/pdf-standard/ua-code-chunk-figure.qmd new file mode 100644 index 00000000000..767da42bd00 --- /dev/null +++ b/tests/docs/smoke-all/pdf-standard/ua-code-chunk-figure.qmd @@ -0,0 +1,35 @@ +--- +title: "Code chunk figure tag 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', '\\begin\{figure\}'] + # Must NOT have \begin{figure}[H] — [H] breaks PDF tag structure + # See https://github.com/quarto-dev/quarto-cli/issues/14164 + - ['\\begin\{figure\}\[H\]'] +--- + +# Code chunk figure with cross-ref + +R code chunk figures with `fig-cap` get automatic `fig-pos='H'` when code is echoed. +The `[H]` specifier (from LaTeX's `float` package) breaks lualatex's PDF tagging, +causing `/Caption` and `/Figure` to be direct children of `/Document` instead of +nested inside `/figures` → `/float`. + +```{r} +#| label: fig-test-plot +#| fig-cap: "Test plot" +plot(1:10) +``` + +See @fig-test-plot. diff --git a/tests/docs/smoke-all/pdf-standard/ua2-unlabeled-figure-caption.qmd b/tests/docs/smoke-all/pdf-standard/ua2-unlabeled-figure-caption.qmd index 78b23fb391c..5f0190dfb5a 100644 --- a/tests/docs/smoke-all/pdf-standard/ua2-unlabeled-figure-caption.qmd +++ b/tests/docs/smoke-all/pdf-standard/ua2-unlabeled-figure-caption.qmd @@ -1,5 +1,5 @@ --- -title: "UA-2: unlabeled figure caption produces invalid structure" +title: "UA-2: unlabeled figure caption" lang: en format: pdf: @@ -14,34 +14,15 @@ _quarto: 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 as a sibling of
under instead of - # nesting them inside a grouping element. This violates UA-2 which - # requires to be a child of
, , or . - # - # 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" + # Must NOT have \begin{figure}[H] — [H] breaks PDF tag structure + # See https://github.com/quarto-dev/quarto-cli/issues/14164 + - ['\\begin\{figure\}\[H\]'] --- -# Known LaTeX tagging limitation +# Unlabeled captioned figure -An unlabeled captioned figure produces invalid UA-2 structure because -tagpdf does not nest `
` inside a grouping element. +An unlabeled captioned figure. Previously this produced invalid UA-2 +structure because `[H]` placement broke lualatex's tag tree. With +`[htbp]` the tag structure is correct. ![This is a caption on an unlabeled figure](penrose.svg)