Skip to content

Code chunk figures with fig-pos='H' produce broken PDF tag structure with pdf-standard: ua-2 #14164

@gordonwoodhull

Description

@gordonwoodhull

Summary

When a figure uses \begin{figure}[H] (from LaTeX's float package), lualatex's PDF tagging system produces broken tag structure: /Caption and /Figure appear as direct children of /Document instead of being properly nested. This causes PDF/UA-2 validation failure: <Document> shall not contain <Caption>.

This affects multiple code paths that inject [H]:

  1. Code chunk figures — both knitr and Jupyter automatically inject fig-pos='H' when code is echoed
  2. Non-cross-referenceable captioned imagespandoc3_figure.lua injects [H] on figures without #fig- labels
  3. Panel layoutsquarto-post/latex.lua forces [H] on figures inside panels

Cross-referenceable static images (![caption](img){#fig-label}) were not affected only because they happen to not get [H] injected — not because of any structural difference in the LaTeX output.

Reproducer

---
title: "Code chunk figure tag structure"
lang: en
format:
  pdf:
    pdf-standard: ua-2
---

```{r}
#| label: fig-mpg-by-weight
#| fig-cap: "MPG vs weight by cylinder count"
library(ggplot2)
ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
  geom_point() +
  geom_smooth(method = "lm")
```

See @fig-mpg-by-weight.

Expected: verapdf validation passes.
Actual: verapdf reports <Document> shall not contain <Caption>.

Root cause

The [H] specifier (from LaTeX's float package) redefines the figure environment in a way that's incompatible with lualatex's PDF tagging system. Standard LaTeX placement specifiers use the native float mechanism, which the tagging system handles correctly.

The LaTeX output is structurally identical with and without [H] — both produce \begin{figure}...\caption{...}\end{figure}. The difference is entirely in how lualatex's tagging system processes the float internally.

Where [H] is injected

  • Knitr: src/resources/rmd/hooks.R:661-667 — sets fig-pos='H' when echo=TRUE, no user fig-pos, no layout options, and LaTeX output
  • Jupyter: src/core/jupyter/jupyter.ts:1625-1631 — same logic
  • Non-cross-referenceable figures: src/resources/filters/layout/pandoc3_figure.lua:126-132 — sets fig-pos='H' on figures with empty identifier
  • Panel layouts: src/resources/filters/quarto-post/latex.lua — forces [H] on floats inside panels

Empirical testing of placement specifiers

Tested each placement specifier with pdf-standard: ua-2 and verapdf validation:

Specifier verapdf result
(none) PASSED
[h] PASSED
[htbp] PASSED
[H] FAILED<Document> shall not contain <Caption>

Only [H] (from the float package) breaks tagging. All standard LaTeX specifiers work correctly.

Proposed fix

When PDF tagging is active (pdf-standard includes a standard that requires tagging, like ua-2), replace fig-pos='H' with fig-pos='h' across all injection points. [h] is the closest semantic equivalent to [H] ("place here") without the float package's incompatible reimplementation.

Workaround

Setting echo: false on code chunks avoids the [H] injection for that specific case, since the placement logic only triggers when code is echoed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilitylatexLaTeX engines related libraries and technologies

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions