Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

LaTeX & Typst (papers, theses, resumes and slides)  Module 7 – Slides, posters and handouts

Accessible slides, posters and handouts

Make slides and posters that pass PDF/UA checks with ltx-talk or Typst, measure theme contrast, and decide whether to share slides, handout or text.

  • Advanced
  • 25 minutes
  • Examples run with TeX Live 2026, TinyTeX 2026.10, Typst 0.15.1, Python 3.14.8, Pyodide 314.0.7, poppler 26.09.0, verapdf 1.30.2, ltx-talk 0.6.7 and touying 0.8.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Build a slide deck that passes a PDF/UA check, with ltx-talk or with Typst, and read its structure tree
  • Measure theme colours against WCAG contrast ratios and keep meaning from resting on colour alone
  • Design a conference poster on one large tagged page in Typst or LaTeX
  • Decide what to share after a talk, the slides, the handout or a text version, for each kind of reader

Before you start

What you’ll learn · Before you start

What you will learn

  • Build a slide deck that passes a PDF/UA check, with ltx-talk or with Typst, and read its structure tree
  • Measure theme colours against WCAG contrast ratios and keep meaning from resting on colour alone
  • Design a conference poster on one large tagged page in Typst or LaTeX
  • Decide what to share after a talk, the slides, the handout or a text version, for each kind of reader

Before you start

On this page

The talk of the Beamer lesson failed 7 of the 106 rules of veraPDF’s PDF/UA-1 check, starting with the most basic one: its PDF has no structure tree, so a screen reader has nothing to follow but the order in which text was drawn. The same talk in the ltx-talk class passed all 1,727 rules of the PDF/UA-2 check, and in Typst it passed PDF/UA-1 once its equations had alternative text.

Below you will check a deck with veraPDF, port it to ltx-talk, export the Typst version to PDF/UA-1, measure the contrast of three themes’ colours, build an A0 poster in both systems and decide which PDF to share with whom. Passing a validator is where the work starts: every structure tree on this page is printed, so you can see what a listener would hear.

A validator shows what a PDF gives a screen reader

A PDF/UA: ISO 14289, the standard for accessible PDF files: tagged content, a title and a structure that assistive software can follow. PDF/UA-1 builds on PDF 1.7 and PDF/UA-2 on PDF 2.0. More in “A one-page resume in LaTeX that parses” file tells assistive software what its text is: headings, paragraphs, lists, formulas, in reading order, in a structure tree: The tree of tags in a tagged PDF, such as headings, paragraphs, lists and links, in reading order, which screen readers follow. pdfinfo -struct-text prints it with the text of each tag. More in “A one-page resume in LaTeX that parses”. veraPDF checks a PDF against a validation profile: The set of rules that veraPDF checks a file against, one profile for each part and level of PDF/A or PDF/UA, such as the 146 rules of PDF/A-2u. More in “Archival PDF/A and final submission checks”, here PDF/UA-1, and reports every rule it fails. vera.py runs it and prints the verdict and the failed rules; the deck is talk.tex of the Beamer lesson, unchanged:

The Beamer talk, and a short report of veraPDF's checks

talk/talk-beamer.tex

\documentclass[aspectratio=169]{beamer}
\setbeamertemplate{navigation symbols}{}

\title{How much rain one roof can catch}
\author{R. Menon}
\institute{Station A rain gauge team}
\date{}

\begin{document}

\begin{frame}
  \titlepage
\end{frame}

\section{The question}

\begin{frame}{Three questions}
  \begin{itemize}[<+->]
    \item How much rain falls on the roof in July?
    \item How much of it reaches the pipe?
    \item Where does the water go?
  \end{itemize}
\end{frame}

\section{The answer}

\begin{frame}[label=formula]{The volume a roof catches}
  \[ V = A \times R \times C \]
  \pause
  Roof area $A = 120~\mathrm{m^2}$, July rain $R = 0.655~\mathrm{m}$,
  runoff share $C = 0.8$.
  \pause
  \[ V = \alert<3>{62.88~\mathrm{m^3}}, \text{ about 62,880 litres.} \]
\end{frame}

\begin{frame}{Where the water goes}
  A 10,000-litre tank holds part of it.

  \only<1| handout:0>{The tank fills in the first rainy week.}
  \only<2>{The other 52,880 litres go to a recharge pit.}

  \uncover<2>{Plan the pit before the tank.}
\end{frame}

\end{document}

vera.py

"""Checks PDFs with veraPDF against one PDF/UA or PDF/A profile and prints a short report for each file: the profile,
the verdict, how many of the profile's rules passed and failed, and each failed rule with its clause and test number,
how many checks failed and veraPDF's description of the rule.

    python3 vera.py ua1 talk.pdf other.pdf

The first argument is veraPDF's name for the profile (ua1, ua2, 2b …). veraPDF writes a JSON report; it also holds
times and build dates, which change on every run, so only the results are printed, and the failed rules in the order
of the standard's clauses.
"""

import json
import subprocess
import sys


def main(flavour, pdfs):
    done = subprocess.run(["verapdf", "--format", "json", "--flavour", flavour, *pdfs], capture_output=True, text=True)
    report = json.loads(done.stdout)["report"]
    for job in report["jobs"]:
        name = job["itemDetails"]["name"].rsplit("/", 1)[-1]
        results = job["validationResult"]
        result = results[0] if isinstance(results, list) else results
        details = result["details"]
        verdict = "compliant" if result["compliant"] else "NOT compliant"
        print(f"{name}: {result['profileName']}: {verdict}, "
              f"{details['passedRules']} rules passed, {details['failedRules']} failed")
        rules = sorted(details["ruleSummaries"], key=lambda r: ([int(n) for n in r["clause"].split(".") if n.isdigit()],
                                                              r["testNumber"]))
        for rule in rules:
            checks = f"{rule['failedChecks']} check" + ("s" if rule["failedChecks"] != 1 else "")
            print(f"  {rule['clause']}-{rule['testNumber']} ({checks}): {rule['description']}")


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2:])
The Beamer talk against PDF/UA-1 Python · check_session.py
from session import files, require, run

# The talk of the Beamer lesson, unchanged, against veraPDF's PDF/UA-1 profile: vera.py prints the verdict and each
# rule the PDF fails.
SIZES = [(r"\((\d+ pages?), \d+ bytes\)", r"(\1)")]
require("verapdf")
files("talk/talk-beamer.tex", "vera.py")
run("""
latexmk -pdf -interaction=nonstopmode -halt-on-error talk-beamer.tex > /dev/null 2>&1
pdfinfo talk-beamer.pdf | grep -E "^(Pages|Tagged)"
python3 vera.py ua1 talk-beamer.pdf
""", mask=SIZES)

Output

$ latexmk -pdf -interaction=nonstopmode -halt-on-error talk-beamer.tex > /dev/null 2>&1
$ pdfinfo talk-beamer.pdf | grep -E "^(Pages|Tagged)"
Tagged:          no
Pages:           9
$ python3 vera.py ua1 talk-beamer.pdf
talk-beamer.pdf: PDF/UA-1 validation profile: NOT compliant, 99 rules passed, 7 failed
  6.2-1 (1 check): The document catalog dictionary shall include a MarkInfo dictionary containing an entry, Marked, whose value shall be true
  7.1-3 (140 checks): Content shall be marked as Artifact or tagged as real content
  7.1-8 (1 check): The Catalog dictionary of a conforming file shall contain the Metadata key whose value is a metadata stream as defined in ISO 32000-1:2008, 14.3.2. The metadata stream dictionary shall contain entry Type with value /Metadata and entry Subtype with value /XML
  7.1-10 (1 check): The document catalog dictionary shall include a ViewerPreferences dictionary containing a DisplayDocTitle key, whose value shall be true
  7.1-11 (1 check): The logical structure of the conforming file shall be described by a structure hierarchy rooted in the StructTreeRoot entry of the document catalog dictionary, as described in ISO 32000-1:2008, 14.7
  7.2-2 (2 checks): Natural language in the Outline entries shall be determined
  7.2-34 (131 checks): Natural language for text in page content shall be determined

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_session.py

pdfinfo said Tagged: no, and 99 rules passed while 7 failed. The seven fall into three groups:

What is missing The failed rules What a listener gets
A structure tree, and the mark that says the PDF has one 7.1-11, 6.2-1 no headings, lists or reading order
Tags on the content 7.1-3, in 140 checks text with no part in any structure
A language, a document title shown as title, XMP metadata 7.2-34 in 131 checks, 7.2-2, 7.1-10, 7.1-8 a guessed language, a file name for a title

None of these can be fixed with a theme or an option. The LaTeX Project’s tagging status list gives Beamer “no-support”: its internals rebox material in a way no tagging can follow, and the list recommends the ltx-talk class for new tagged presentations.

A check, not a verdict on the slides

veraPDF tests what a machine can test: that tags exist, that the language and title are set, that images and formulas have text. Whether the tags make sense to a listener is a separate question, which the structure trees below answer.

Checkpoint

Check: What a PDF/UA check finds

  1. Question 1 of 2 The Beamer talk failed rule 7.1-11 of PDF/UA-1. What does that rule ask for?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A structure tree, the tags in reading order that assistive software follows

    Without a structure tree there is nothing to tag, which is why the other failures follow; no theme or option of Beamer adds one.

  2. Question 2 of 2 Which veraPDF profiles check the two tagged decks of this lesson?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ua2 for the ltx-talk deck, which asks for PDF/UA-2, and ua1 for the Typst deck

    Check a PDF against the standard it says it meets: ua2 for PDF/UA-2, ua1 for PDF/UA-1.

ltx-talk keeps Beamer’s frames and tags the last slide of each

ltx-talk is a new class whose syntax is close to Beamer’s and whose manual makes tagged output its aim. It requires \DocumentMetadata, works best with LuaLaTeX and does not support XeTeX. The port of the talk changed two places, as diff prints them: the first two lines, and the title frame, which \maketitle makes:

The talk in ltx-talk, and a short printer for structure trees

talk/talk-ltxtalk.tex

\DocumentMetadata{lang = en, pdfstandard = ua-2, tagging = on}
\documentclass[frame-title-arg]{ltx-talk}

\title{How much rain one roof can catch}
\author{R. Menon}
\institute{Station A rain gauge team}
\date{}

\begin{document}

\maketitle

\section{The question}

\begin{frame}{Three questions}
  \begin{itemize}[<+->]
    \item How much rain falls on the roof in July?
    \item How much of it reaches the pipe?
    \item Where does the water go?
  \end{itemize}
\end{frame}

\section{The answer}

\begin{frame}[label=formula]{The volume a roof catches}
  \[ V = A \times R \times C \]
  \pause
  Roof area $A = 120~\mathrm{m^2}$, July rain $R = 0.655~\mathrm{m}$,
  runoff share $C = 0.8$.
  \pause
  \[ V = \alert<3>{62.88~\mathrm{m^3}}, \text{ about 62,880 litres.} \]
\end{frame}

\begin{frame}{Where the water goes}
  A 10,000-litre tank holds part of it.

  \only<1| handout:0>{The tank fills in the first rainy week.}
  \only<2>{The other 52,880 litres go to a recharge pit.}

  \uncover<2>{Plan the pit before the tank.}
\end{frame}

\end{document}

tree.py

"""Prints the structure tree that `pdfinfo -struct-text` reads from a tagged PDF, given on standard input, in short:
one line per tag, indented as in the tree, with the text the tag holds in quotes. The attributes poppler prints under
a tag and the numbers it gives each tag are left out, so what is left is what a screen reader follows: the tags, in
reading order, and their words. A tag that holds no text but has a title, as some heading tags do, shows the title in
square brackets; poppler prints such titles as UTF-16 bytes, which are decoded here.

    pdfinfo -struct-text talk.pdf | python3 tree.py
"""

import re
import sys

UTF16_TITLE = re.compile(rb'"\xfe\xff((?:[\x00-\xff][\x00-\xff])*?)"')

tags = []  # [depth, tag, title, text]
for raw in sys.stdin.buffer.read().splitlines():
    title_match = UTF16_TITLE.search(raw)
    title = title_match.group(1).decode("utf-16-be", errors="replace").strip() if title_match else ""
    line = UTF16_TITLE.sub(b"", raw).decode("utf-8", errors="replace")
    body = line.lstrip(" ")
    depth = (len(line) - len(body)) // 2
    if not body or body.startswith(("/", "Syntax", "Object ")):
        continue
    if body.startswith('"'):
        text = body.strip().strip('"').strip()
        owner = next((t for t in reversed(tags) if t[0] < depth), None)
        if text and owner:
            owner[3] = (owner[3] + " " + text).strip()
        continue
    found = re.match(r"(\w+)", body)
    if found:
        tags.append([depth, found.group(1), title, ""])

if not tags:
    print("no structure tree: the PDF is not tagged")
for depth, tag, title, text in tags:
    line = "  " * depth + tag
    if text:
        line += f' "{text}"'
    elif title:
        line += f" [{title}]"
    print(line)
What changed, the PDF/UA-2 check and the structure tree Python · ltxtalk_session.py
from session import files, require, run

# The same talk in the ltx-talk class: what changed against the Beamer source, the build with LuaLaTeX, veraPDF's
# PDF/UA-2 profile, the structure tree (tree.py) and how often two of the talk's lines are in the page text and in the
# tags. LuaLaTeX's first run in a new home folder also builds its font name database, which takes a while.
require("verapdf")
files("talk/talk-beamer.tex", "talk/talk-ltxtalk.tex", "vera.py", "tree.py")
run("""
!diff talk-beamer.tex talk-ltxtalk.tex
latexmk -lualatex -interaction=nonstopmode -halt-on-error talk-ltxtalk.tex > /dev/null 2>&1
pdfinfo talk-ltxtalk.pdf | grep -E "^(Pages|Page size|Tagged|PDF version)"
python3 vera.py ua2 talk-ltxtalk.pdf
pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | python3 tree.py
pdftotext talk-ltxtalk.pdf - | grep -c "How much rain falls"
pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "How much rain falls"
pdftotext talk-ltxtalk.pdf - | grep -c "The tank fills"
!pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "The tank fills"
""")

Output

$ diff talk-beamer.tex talk-ltxtalk.tex
1,2c1,2
< \documentclass[aspectratio=169]{beamer}
< \setbeamertemplate{navigation symbols}{}
---
> \DocumentMetadata{lang = en, pdfstandard = ua-2, tagging = on}
> \documentclass[frame-title-arg]{ltx-talk}
11,13c11
< \begin{frame}
<   \titlepage
< \end{frame}
---
> \maketitle
[exit status 1]
$ latexmk -lualatex -interaction=nonstopmode -halt-on-error talk-ltxtalk.tex > /dev/null 2>&1
$ pdfinfo talk-ltxtalk.pdf | grep -E "^(Pages|Page size|Tagged|PDF version)"
Tagged:          yes
Pages:           9
Page size:       503.937 x 283.465 pts
PDF version:     2.0
$ python3 vera.py ua2 talk-ltxtalk.pdf
talk-ltxtalk.pdf: PDF/UA-2 + Tagged PDF validation profile: compliant, 1727 rules passed, 0 failed
$ pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | python3 tree.py
Document
  Sect
    Part
      P
        P "How much rain one roof can catch"
      P "R. Menon"
      P "Station A rain gauge team"
  Sect
    H1 [The question]
    Sect
      H4 "Three questions"
      Part
        L
          LI
            Lbl "•"
            LBody
              Part
                P "How much rain falls on the roof in July?"
          LI
            Lbl "•"
            LBody
              Part
                P "How much of it reaches the pipe?"
          LI
            Lbl "•"
            LBody
              Part
                P "Where does the water go?"
  Sect
    H1 [The answer]
    Sect
      H4 "The volume a roof catches"
      Part
        Formula "𝑉=𝐴×𝑅×𝐶"
        P "Roof area , July rain , runoff share ."
          Formula "𝐴=120m2"
          Formula "𝑅=0.655m"
          Formula "𝐶=0.8"
        Formula "𝑉=62.88m3, about 62,880 litres."
    Sect
      H4 "Where the water goes"
      Part
        P "A 10,000-litre tank holds part of it."
      Part
        P "The other 52,880 litres go to a recharge pit."
      Part
        P "Plan the pit before the tank."
$ pdftotext talk-ltxtalk.pdf - | grep -c "How much rain falls"
3
$ pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "How much rain falls"
1
$ pdftotext talk-ltxtalk.pdf - | grep -c "The tank fills"
1
$ pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "The tank fills"
0
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ltxtalk_session.py

The PDF is version 2.0, tagged, 9 pages of 503.937 by 283.465 points: the manual’s slides are 100 mm high, here at 16:9. veraPDF’s PDF/UA-2 profile passed every one of its 1,727 rules. The tree is the useful part. Step through it as a screen reader would:

Interactive · stage by stage What a screen reader meets in talk-ltxtalk.pdf Step through the structure tree of the ltx-talk deck in reading order, from the title page to the last frame.
What a screen reader meets in talk-ltxtalk.pdf, step 7 of 7. Showing P: title, author, institute, H1: The question, H4: Three questions, L: three items, H4: The volume a roof…, H4: Where the water goes, The tank fills… (in no tag: an artifact). In focus: The tank fills….P: title, author, institutethe title pageH1: The questionits name as a title onlyH4: Three questionsa frame titleL: three itemseach question onceH4: The volume a roof…two Formula tags and a PH4: Where the water goesthree P tagsThe tank fills…in no tag: an artifact

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The title page. The tree starts with three paragraphs, the title, the author and the institute, as \maketitle set them.
  2. A section. \section{The question} becomes an H1 tag. The class prints no section page, so the tag carries the name only as its title.
  3. Frame titles are H4. Each frame title is an H4 tag, deliberately below sections and subsections, as the class manual says. Some checkers may warn about the jump from H1 to H4.
  4. Each item once. The frame made three slides, and pdftotext found the first question 3 times in the page text. The tags hold it once, because only the last slide of a frame is tagged; the others are artifacts.
  5. Formulas. The volume frame has a Formula tag for each display equation and one for each inline formula, inside the paragraph of numbers.
  6. The last frame. The last frame is tagged from its last slide, slide 2, so a listener hears the tank, the recharge pit and the plan for the pit.
  7. What is never heard. "The tank fills in the first rainy week." was only on slide 1, so the tags never hold it: grep found it once in the page text and 0 times in the tags.

The manual explains the counts: only the last slide of each frame is tagged, and the others are marked as artifacts (its tag-slides option, set to n, the last slide). That is right for a list that builds up, whose last slide holds every item, and wrong for the \only<1| handout:0> line, which a listener never hears.

A line that only an early slide shows

Wrong: putting a sentence that matters in \only<1>, so that it is on slide 1 alone, which ltx-talk tags as an artifact. Right: keep it on the last slide of its frame, with \uncover for the lines that come later, or list the slides to tag in the frame’s tag-slides option.

Checkpoint

Check: Tagged slides with ltx-talk

  1. Question 1 of 2 In talk-ltxtalk.pdf, "Three questions" is one frame of three slides, each adding a question. What does a screen reader following the tags read?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Each question once, from the frame's last slide

    A list that builds up is right in ltx-talk's tags; a line that only an early slide shows is lost from them.

  2. Question 2 of 2 Which engine does the ltx-talk manual recommend for tagged output?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: LuaTeX, through lualatex

    Build ltx-talk decks with latexmk -lualatex, as the session did.

A Typst deck passes PDF/UA-1 once every equation has a description

Typst writes tagged PDF by default, and --pdf-standard ua-1 makes the export refuse what PDF/UA-1 forbids. Its accessibility guide lists what that means: a document title, and an alternative description for every image and every equation, written as you would read the formula aloud. The first export of the Touying talk stopped, and talk-touying-ua.typ fixes it:

The Touying talk as it was, and with descriptions, a language and a darker header

talk/talk-touying-ua.typ

#import "@preview/touying:0.8.0": *
#import themes.simple: *

#set document(title: [How much rain one roof can catch], author: "R. Menon")
#set text(lang: "en")
#show: simple-theme.with(
  aspect-ratio: "16-9",
  config-common(
    new-section-slide-fn: none,
    // In article mode, no title block from the article theme: the title slide's lines open the text.
    article-theme: themes.article.article-theme.with(title-block-fn: (..args) => none),
  ),
  // Header and footer text darker than the theme's gray, and alerts bold as well as coloured.
  config-colors(neutral-light: rgb("#595959")),
  config-methods(alert: (self: none, it) => text(fill: self.colors.primary, weight: "bold", it)),
)

#title-slide[
  #text(1.4em, weight: "bold")[How much rain one roof can catch]

  R. Menon, Station A rain gauge team
]

= The question

== Three questions

- How much rain falls on the roof in July?
#pause
- How much of it reaches the pipe?
#pause
- Where does the water go?

= The answer

== The volume a roof catches

#math.equation(block: true, alt: "V equals A times R times C", $V = A times R times C$)
#pause
Roof area _A_ = 120 m², July rain _R_ = 0.655 m, runoff share _C_ = 0.8.
#pause
#math.equation(
  block: true,
  alt: "V equals 62.88 cubic metres, about 62,880 litres",
  $V = #alert[$62.88 "m"^3$], "about 62,880 litres." $,
)

== Where the water goes

A 10,000-litre tank holds part of it.

// Every line stays on the last subslide, which the handout and the text version keep.
The tank fills in the first rainy week.

#uncover(2)[The other 52,880 litres go to a recharge pit.]

#uncover(2)[Plan the pit before the tank.]

talk/talk-touying.typ

#import "@preview/touying:0.8.0": *
#import themes.simple: *

#set document(title: [How much rain one roof can catch], author: "R. Menon")
#show: simple-theme.with(
  aspect-ratio: "16-9",
  config-common(new-section-slide-fn: none),
)

#title-slide[
  #text(1.4em, weight: "bold")[How much rain one roof can catch]

  R. Menon, Station A rain gauge team
]

= The question

== Three questions

- How much rain falls on the roof in July?
#pause
- How much of it reaches the pipe?
#pause
- Where does the water go?

= The answer

== The volume a roof catches

$ V = A times R times C $
#pause
Roof area $A = 120 "m"^2$, July rain $R = 0.655 "m"$, runoff share $C = 0.8$.
#pause
$ V = #alert[$62.88 "m"^3$], "about 62,880 litres." $

== Where the water goes

A 10,000-litre tank holds part of it.

#only(1)[The tank fills in the first rainy week.]
#only(2)[The other 52,880 litres go to a recharge pit.]

#uncover(2)[Plan the pit before the tank.]
The Touying talk exported as PDF/UA-1, before and after Python · typst_session.py
from session import files, packages, require, run

# The Touying talk of the Typst lesson exported as PDF/UA-1, first as it was, then talk-touying-ua.typ: alternative
# text on the two display equations, the inline maths written as text, the language, a darker header and bold alerts.
# Then veraPDF's PDF/UA-1 profile, the structure tree and how often a line of the talk is in the tags.
require("verapdf")
packages("touying:0.8.0", "uniwarn:0.1.1")
files("talk/talk-touying.typ", "talk/talk-touying-ua.typ", "vera.py", "tree.py")
run("""
!typst compile --pdf-standard ua-1 talk-touying.typ
typst compile --pdf-standard ua-1 talk-touying-ua.typ
python3 vera.py ua1 talk-touying-ua.pdf
pdfinfo -struct-text talk-touying-ua.pdf | python3 tree.py | sed -n 1,30p
pdfinfo -struct-text talk-touying-ua.pdf | grep -c "How much rain falls"
""")

Output

$ typst compile --pdf-standard ua-1 talk-touying.typ
error: PDF/UA-1 error: missing alt text
    ┌─ @preview/touying:0.8.0/src/core/tree.typ:336:2
    │
336 │   f(..leading, ..fields, ..extra)
    │   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    │
    = hint: make sure your images and equations have alt text

[exit status 1]
$ typst compile --pdf-standard ua-1 talk-touying-ua.typ
$ python3 vera.py ua1 talk-touying-ua.pdf
talk-touying-ua.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ pdfinfo -struct-text talk-touying-ua.pdf | python3 tree.py | sed -n 1,30p
Document
  P "How much rain one roof can catch"
  P "R. Menon, Station A rain gauge team"
  H1
  H2
  Span "Three questions"
  L
    LI
      Lbl "•"
      LBody "How much rain falls on the roof in July?"
  Span "Three questions"
  L
    LI
      Lbl "•"
      LBody "How much rain falls on the roof in July?"
    LI
      Lbl "•"
      LBody "How much of it reaches the pipe?"
  Span "Three questions"
  L
    LI
      Lbl "•"
      LBody "How much rain falls on the roof in July?"
    LI
      Lbl "•"
      LBody "How much of it reaches the pipe?"
    LI
      Lbl "•"
      LBody "Where does the water go?"
  H1
$ pdfinfo -struct-text talk-touying-ua.pdf | grep -c "How much rain falls"
3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typst_session.py

The error, “PDF/UA-1 error: missing alt text”, pointed at a line inside Touying, tree.typ:336, not at the equation: Touying rebuilds slide content, so the place it names is in the package. The fix gives both display equations an alt through math.equation and writes the inline quantities as text, “A = 120 m²”, which needs no description. With that the export finished, and veraPDF passed all 106 rules of PDF/UA-1. #set text(lang: "en") names the language as well: Typst assumes English when a document does not say, and its guide asks every document to state its language.

The tree shows what the check does not judge. Every subslide is tagged, so the first question is in the tags 3 times, once per step, and the H1 and H2 heading tags hold no words: the slide titles are drawn in Span tags. Set beside the ltx-talk tree, the two passing decks give a listener different things:

In the structure tree ltx-talk Typst with Touying
Section headings H1, the name as a title H1, empty
Slide titles H4, with their words Span, below an empty H2
A list built up in 3 steps once 3 times

Key idea

A deck that passes PDF/UA can still repeat itself and hide its headings: read the structure tree, and share a version made for listeners as well as the slides.

Checkpoint

Check: Typst decks exported as PDF/UA-1

  1. Question 1 of 2 typst compile --pdf-standard ua-1 on the Touying talk stopped with "missing alt text" at @preview/touying:0.8.0/src/core/tree.typ:336. What is wrong?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An equation of the talk has no description; Touying rebuilds slide content, so the error names a line inside the package

    When an error points into a package, look for what its message names in your own content.

  2. Question 2 of 2 A Touying slide reveals 4 items one by one with #pause, exported as PDF/UA-1. How many times is the first item in the tags of the slides PDF?

    Type a number.

    A hint

    Typst tags every subslide, and the first item is on all of them.

    Show the answer: Question 2 of 2

    Answer: 4 times

    The talk's three-step list put its first question in the tags 3 times; four steps put it there 4 times. The handout and the text version hold it once.

Contrast and colour decide who can read the slides

Under WCAG 2.2, text needs 4.5:1 or more against its background, large text (18 points, or 14 points bold) and marks such as chart lines need 3:1, and colour may never be the only way information is shown. A contrast ratio: WCAG's measure of how far apart two colours are in luminance, from 1:1 to 21:1, worked out as (L1 + 0.05) / (L2 + 0.05). Text needs at least 4.5:1, large text and graphics 3:1. compares how light two colours are: two identical colours give 1:1, and black text on white gives the highest value, 21:1. contrast.py works it out with the formulas WCAG defines, for colour pairs taken from the theme files:

Contrast ratios of three themes' colours, and two fixes Python · contrast/contrast.py
"""Contrast ratios of slide-theme colours, worked out with the formulas of WCAG 2.2.

Each pair is a text colour on the background it sits on, as the theme files define them: Beamer's default colour
theme (beamercolorthemedefault.sty), the metropolis colour theme (beamercolorthememetropolis.sty) and Touying's
simple theme (themes/simple.typ). A colour a theme gives in fractions is rounded to whole channel values, half up:
Beamer's rgb 0.2, 0.2, 0.7 is #3333B3, and Touying's aqua.darken(50%), #7FDBFF with each channel halved, is #406E80.
The last two pairs are the fixes this lesson uses. Change a pair, or add your own, and run it again.
"""


def channel(value):
    """One sRGB channel, 0 to 255, as the linear value the luminance formula needs."""
    c = value / 255
    return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4


def luminance(hex_colour):
    """Relative luminance of a colour written as #RRGGBB: 0 for black, 1 for white."""
    r, g, b = (int(hex_colour[i:i + 2], 16) for i in (1, 3, 5))
    return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)


def contrast(foreground, background):
    """The contrast ratio of two colours, from 1 (the same) to 21 (black on white)."""
    lighter, darker = sorted((luminance(foreground), luminance(background)), reverse=True)
    return (lighter + 0.05) / (darker + 0.05)


PAIRS = [
    ("Beamer default", "body text", "#000000", "#FFFFFF"),
    ("Beamer default", "frame title", "#3333B3", "#FFFFFF"),
    ("Beamer default", "\\alert text", "#FF0000", "#FFFFFF"),
    ("metropolis", "body text", "#23373B", "#FAFAFA"),
    ("metropolis", "\\alert text", "#EB811B", "#FAFAFA"),
    ("metropolis", "example text", "#14B03D", "#FAFAFA"),
    ("Touying simple", "body text", "#000000", "#FFFFFF"),
    ("Touying simple", "#alert text", "#406E80", "#FFFFFF"),
    ("Touying simple", "header and footer", "#AAAAAA", "#FFFFFF"),
    ("Beamer, fixed", "\\alert, red!80!black", "#CC0000", "#FFFFFF"),
    ("Touying, fixed", "header and footer", "#595959", "#FFFFFF"),
]

print(f"{'theme':<16}{'text':<21}{'ratio':>7}  4.5:1  3:1")
for theme, text, foreground, background in PAIRS:
    ratio = contrast(foreground, background)
    small = "yes" if ratio >= 4.5 else "no"
    large = "yes" if ratio >= 3 else "no"
    print(f"{theme:<16}{text:<21}{ratio:>7.2f}  {small:<5}  {large}")

Output

theme           text                   ratio  4.5:1  3:1
Beamer default  body text              21.00  yes    yes
Beamer default  frame title             9.27  yes    yes
Beamer default  \alert text             4.00  no     yes
metropolis      body text              11.97  yes    yes
metropolis      \alert text             2.62  no     no
metropolis      example text            2.75  no     no
Touying simple  body text              21.00  yes    yes
Touying simple  #alert text             5.58  yes    yes
Touying simple  header and footer       2.32  no     no
Beamer, fixed   \alert, red!80!black    5.89  yes    yes
Touying, fixed  header and footer       7.00  yes    yes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 contrast.py

Beamer’s default \alert red reached 4.00:1, enough only for large text. metropolis set its alerts at 2.62:1 and its example text at 2.75:1, below even the 3:1 floor, and the grey header and footer of Touying’s simple theme, #aaaaaa, reached 2.32:1. Slide text is read at many sizes, projected, on a phone and four to a printed page, so 4.5:1 is the safe bar for all of it. The fixes are one line each: \setbeamercolor{alerted text}{fg=red!80!black}, the example colour of Beamer’s own guide, gave 5.89:1, and config-colors(neutral-light: rgb("#595959")) in talk-touying-ua.typ gave the header 7.00:1.

Test your own theme

Press Edit, add a line to PAIRS with your theme’s text and background colours, and run it again. A “no” in the 4.5:1 column means the colour must be darker, unless the text is large, when the 3:1 column decides.

Colour alone is the second trap. \alert and Touying’s #alert change only the colour, so a reader who cannot tell red from black, or who prints in greyscale, loses the emphasis: talk-touying-ua.typ makes alerts bold as well. A chart whose two lines differ only by red and green needs labels on the lines, or different marks and dashes.

Color Contrast Checker (WCAG 2.2) Check any text and background colour of your theme against the 4.5:1 and 3:1 bars. Color Blindness Simulator See a slide or a chart as readers with the common colour-vision deficiencies see it.

Checkpoint

Check: Contrast ratios and colour alone

  1. Question 1 of 2 metropolis sets \alert text at 2.62:1 against its background. Is that enough for large slide titles?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No; it is below 3:1, the bar for large text and for marks, and far below 4.5:1

    contrast.py found 2.62:1 for the alert and 2.75:1 for the example text; both need darker colours.

  2. Question 2 of 2 Your slides mark the key number with \alert in red. Which change keeps the emphasis for a reader who cannot tell red from black?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Make alerts bold as well as coloured

    talk-touying-ua.typ sets alerts in bold and in colour; in Beamer, \setbeamerfont{alerted text}{series=\bfseries} does the same.

A poster is one large tagged page with its columns in reading order

A conference poster is a single page, usually A0, 841 by 1189 mm, with a title band and a few columns. In Typst it is a page size and a grid; the chart keeps its alternative text and its caption, and the equation its description. In LaTeX the usual poster tools are a problem for tagging: the beamerposter package builds on Beamer, and the tagging status list gives it no support, while the tikzposter class is not on the list at all. poster.tex uses the article class instead, with geometry for the A0 page, multicol for the columns and unicode-math for the formula, all of them marked compatible in the list, and sets its sizes by redefining \normalsize, \large, \Large and \LARGE:

The talk as an A0 poster, in Typst and in LaTeX

poster/poster.typ

// An A0 poster: one page, a title band and three columns, read left to right.
#set document(title: [How much rain one roof can catch], author: "R. Menon")
#set text(lang: "en", size: 32pt)
#set page(paper: "a0", margin: 3cm)
#set heading(numbering: none)
#show heading: set text(size: 54pt)

#show title: set text(size: 110pt)

#align(center)[
  #title()
  #text(48pt)[R. Menon, Station A rain gauge team]
]

#v(2cm)

#grid(
  columns: (1fr, 1fr, 1fr),
  column-gutter: 2.5cm,
  [
    = The question

    How much rain falls on one roof in July, how much of it reaches the pipe, and where does the water go?

    #figure(
      image("rain.svg", width: 100%, alt: "Bar chart of monthly rainfall at Station A: under 65 mm a month from December to May, a peak of 655 mm in July."),
      caption: [Monthly rainfall at Station A, an invented station.],
    )
  ],
  [
    = The answer

    #math.equation(block: true, alt: "V equals A times R times C", $V = A times R times C$)

    A roof of 120 m², the 0.655 m of July rain and a runoff share of 0.8 give 62.88 m³: about 62,880 litres.
  ],
  [
    = Where the water goes

    A 10,000-litre tank holds part of it. The other 52,880 litres go to a recharge pit, so plan the pit before the
    tank.
  ],
)

poster/poster.tex

\DocumentMetadata{lang = en, pdfstandard = ua-2, tagging = on}
\documentclass{article}
\usepackage[a0paper, margin = 3cm]{geometry}
\usepackage{multicol}
\usepackage{graphicx}
\usepackage{unicode-math}

% Poster sizes: body text 32 pt, headings 54 pt, the author line 48 pt and the title 110 pt.
\renewcommand{\normalsize}{\fontsize{32}{40}\selectfont}
\renewcommand{\Large}{\fontsize{54}{64}\selectfont}
\renewcommand{\large}{\fontsize{48}{58}\selectfont}
\renewcommand{\LARGE}{\fontsize{110}{124}\selectfont}
\setlength{\columnsep}{2.5cm}
\pagestyle{empty}

\title{How much rain one roof can catch}
\author{R. Menon, Station A rain gauge team}
\date{}

\begin{document}
\normalsize
\maketitle

\begin{multicols}{3}
\section*{The question}
How much rain falls on one roof in July, how much of it reaches the pipe, and where does the water go?

\begin{center}
  \includegraphics[width = \linewidth, alt = {Bar chart of monthly rainfall at Station A: under 65 mm a month
    from December to May, a peak of 655 mm in July.}]{rain}

  Monthly rainfall at Station A, an invented station.
\end{center}

\section*{The answer}
\[ V = A \times R \times C \]
A roof of 120~m², the 0.655~m of July rain and a runoff share of 0.8 give 62.88~m³: about 62,880 litres.

\section*{Where the water goes}
A 10,000-litre tank holds part of it. The other 52,880 litres go to a recharge pit, so plan the pit before the tank.
\end{multicols}
\end{document}
Both posters: page size, reading order and the checks Python · poster_session.py
from session import files, require, run

# Two A0 posters of the talk: poster.typ in Typst, exported as PDF/UA-1, and poster.tex, the article class at A0 with
# multicol, tagged for PDF/UA-2 and built with LuaLaTeX. rsvg-convert turns the chart into a PDF for LaTeX.
SIZES = [(r"\((\d+ pages?), \d+ bytes\)", r"(\1)")]
require("verapdf", "rsvg-convert")
files("poster/poster.typ", "poster/poster.tex", "poster/rain.svg", "vera.py", "tree.py")
run("""
typst compile --pdf-standard ua-1 poster.typ poster-typst.pdf
pdfinfo poster-typst.pdf | grep -E "^(Pages|Page size)"
pdfinfo -struct-text poster-typst.pdf | python3 tree.py
python3 vera.py ua1 poster-typst.pdf
rsvg-convert -f pdf -o rain.pdf rain.svg
latexmk -lualatex -interaction=nonstopmode -halt-on-error poster.tex > /dev/null 2>&1
pdfinfo poster.pdf | grep -E "^(Pages|Page size)"
python3 vera.py ua2 poster.pdf
""", mask=SIZES)

Output

$ typst compile --pdf-standard ua-1 poster.typ poster-typst.pdf
$ pdfinfo poster-typst.pdf | grep -E "^(Pages|Page size)"
Pages:           1
Page size:       2383.94 x 3370.39 pts (A0)
$ pdfinfo -struct-text poster-typst.pdf | python3 tree.py
Document
  P "How much rain one roof can catch"
  P "R. Menon, Station A rain gauge team"
  Div
    Div
      H1 "The question"
      P "How much rain falls on one roof in July, how much of it reaches the pipe, and where does the water go?"
      Div
        Caption
          Span "Figure 1: Monthly rainfall at Station A, an invented station."
        Figure "Monthly rainfall at Station A (mm)0200400600JanFebMarAprMayJunJulAugSepOctNovDec655"
    Div
      H1 "The answer"
      Formula "𝑉 = 𝐴 × 𝑅 × 𝐶"
      P "A roof of 120 m², the 0.655 m of July rain and a runoff share of 0.8 give 62.88 m³: about 62,880 litres."
    Div
      H1 "Where the water goes"
      P "A 10,000-litre tank holds part of it. The other 52,880 litres go to a recharge pit, so plan the pit before the tank."
$ python3 vera.py ua1 poster-typst.pdf
poster-typst.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ rsvg-convert -f pdf -o rain.pdf rain.svg
$ latexmk -lualatex -interaction=nonstopmode -halt-on-error poster.tex > /dev/null 2>&1
$ pdfinfo poster.pdf | grep -E "^(Pages|Page size)"
Pages:           1
Page size:       2383.94 x 3370.39 pts (A0)
$ python3 vera.py ua2 poster.pdf
poster.pdf: PDF/UA-2 + Tagged PDF validation profile: compliant, 1727 rules passed, 0 failed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 poster_session.py

Both pages are 2383.94 by 3370.39 points, A0. The Typst poster passed PDF/UA-1 with its 106 rules and the LaTeX poster PDF/UA-2 with its 1,727. The tree of the Typst poster shows why a grid suits a poster: each column is a Div in the order of the grid’s cells, with its heading, paragraphs, figure and formula inside, so a listener reads the first column to the end before the second. Write the columns in the order they should be read, and keep the body text large: 32 points here, with headings at 54 and the title at 110. The two posters and their checks:

Poster Engine Check Result
poster.typ Typst 0.15.1 PDF/UA-1 compliant, 106 rules
poster.tex LuaLaTeX PDF/UA-2 compliant, 1,727 rules
Print Large PDF on Multiple Pages (Poster / Tiles) Print an A0 poster across A4 sheets to check its text sizes at full scale before you send it to the printer.

Checkpoint

Check: Posters on one tagged page

  1. Question 1 of 2 In what order does a screen reader read the three columns of poster.typ?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Each column to its end, in the order of the grid's cells, left to right

    A grid's cells are its reading order: put them in the order a reader should take them.

  2. Question 2 of 2 A LaTeX poster must pass PDF/UA-2. Which starting point does this lesson's evidence support?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The article class with geometry and multicol, built with LuaLaTeX and tagging on

    Pick classes and packages the tagging status list marks compatible, then confirm with veraPDF.

Share the version that fits each reader

After the talk, people read it again in different ways: some page through it as it was shown, some print it, some listen to it with a screen reader, some search it. One Typst source gives three PDFs for them, each exported as PDF/UA-1: the slides, the handout with --input export-mode=handout, and a text version with --input export-mode=article, which Touying’s changelog describes as the same source compiled to a flowing prose document:

Slides, handout and text version of the same talk Python · share_session.py
from session import files, packages, require, run

# Three PDFs from talk-touying-ua.typ for three readers: the slides, the handout (export-mode=handout) and the text
# version (export-mode=article, one flowing page). For each: pages, and how often one line of the talk is in the page
# text and in the tags. Then the first tags of the handout, the text version's structure tree and its PDF/UA-1 check.
require("verapdf")
packages("touying:0.8.0", "uniwarn:0.1.1")
files("talk/talk-touying-ua.typ", "vera.py", "tree.py")
run("""
typst compile --pdf-standard ua-1 talk-touying-ua.typ slides.pdf
typst compile --pdf-standard ua-1 --input export-mode=handout talk-touying-ua.typ handout.pdf
typst compile --pdf-standard ua-1 --input export-mode=article talk-touying-ua.typ text.pdf
pdfinfo slides.pdf | grep -E "^Pages"
pdftotext slides.pdf - | grep -c "How much rain falls"
pdfinfo handout.pdf | grep -E "^Pages"
pdftotext handout.pdf - | grep -c "How much rain falls"
pdfinfo -struct-text handout.pdf | python3 tree.py | sed -n 1,6p
pdfinfo text.pdf | grep -E "^(Pages|Page size)"
pdftotext text.pdf - | grep -c "How much rain falls"
pdftotext text.pdf - | grep -c "The tank fills"
pdfinfo -struct-text text.pdf | python3 tree.py
python3 vera.py ua1 text.pdf
""")

Output

$ typst compile --pdf-standard ua-1 talk-touying-ua.typ slides.pdf
$ typst compile --pdf-standard ua-1 --input export-mode=handout talk-touying-ua.typ handout.pdf
$ typst compile --pdf-standard ua-1 --input export-mode=article talk-touying-ua.typ text.pdf
$ pdfinfo slides.pdf | grep -E "^Pages"
Pages:           9
$ pdftotext slides.pdf - | grep -c "How much rain falls"
3
$ pdfinfo handout.pdf | grep -E "^Pages"
Pages:           4
$ pdftotext handout.pdf - | grep -c "How much rain falls"
1
$ pdfinfo -struct-text handout.pdf | python3 tree.py | sed -n 1,6p
Document
  P "How much rain one roof can catch"
  P "R. Menon, Station A rain gauge team"
  H1
  H2
  Span "Three questions"
$ pdfinfo text.pdf | grep -E "^(Pages|Page size)"
Pages:           1
Page size:       595.276 x 841.89 pts (A4)
$ pdftotext text.pdf - | grep -c "How much rain falls"
1
$ pdftotext text.pdf - | grep -c "The tank fills"
1
$ pdfinfo -struct-text text.pdf | python3 tree.py
Document
  P "How much rain one roof can catch"
  P "R. Menon, Station A rain gauge team"
  H1 "The question"
  H2 "Three questions"
  L
    LI
      Lbl "•"
      LBody "How much rain falls on the roof in July?"
    LI
      Lbl "•"
      LBody "How much of it reaches the pipe?"
    LI
      Lbl "•"
      LBody "Where does the water go?"
  H1 "The answer"
  H2 "The volume a roof catches"
  Formula "𝑉 = 𝐴 × 𝑅 × 𝐶"
  P "Roof area = 120 m², July rain = 0.655 m, runoff share = 0.8."
    Span "A"
    Span "R"
    Span "C"
  Formula "𝑉 = 6 2 . 8 8 m 3 , about 62,880 litres."
  H2 "Where the water goes"
  P "A 10,000-litre tank holds part of it."
  P "The tank fills in the first rainy week."
  P "The other 52,880 litres go to a recharge pit."
  P "Plan the pit before the tank."
$ python3 vera.py ua1 text.pdf
text.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 share_session.py

The slides held the first question 3 times in 9 pages, the handout once in 4 pages, and the text version: A talk written out as a flowing document, with real headings and each line once, such as Touying's article mode gives, for readers who listen with a screen reader or search. once on a single A4 page, with every line of the last slide, the tank included, because talk-touying-ua.typ keeps each line on the last step. The handout’s tree starts as the slides’ does, with an empty H1 and H2 before the first slide title. The text version’s tree is the one a listener needs: H1 sections and H2 slide titles, each with its words, and every line once. It passed PDF/UA-1 as well. For a LaTeX talk the same choices exist: Beamer’s guide describes an article version of a talk next to the handout, and ltx-talk has a handout mode. Answer two questions about your readers:

Interactive · answer and compare Which PDF to share with whom? Answer two questions about the people who will read the talk afterwards; the table marks the versions that fit.
Will some readers use a screen reader, or jump through the document by its headings?
Do readers need the slides as they looked, with their layout and pictures?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

The slides, the handout and the text version of the same Typst talk
Version Each line once Looks like the talk Headings with words Pages
Slides Made with: typst compile Poor the first question 3 times, once per step Good every step, as the audience saw it Poor empty H1 and H2 tags; slide titles are Span tags Mixed 9 pages of 297 by 167 mm
Handout Made with: --input export-mode=handout Good each line once, from the last step of each slide Good one page per slide, as the slide ended Poor the same empty heading tags as the slides Good 4 pages
Text version Made with: --input export-mode=article Good each line once Poor one flowing A4 page, without the slide design Good H1 sections and H2 slide titles, with their words Good 1 A4 page
How the answers decide
  • “Will some readers use a screen reader, or jump through the document by its headings?” Yes, or I cannot rule it out: rules out Slides and adds a caveat to Handout. A listener hears each step of the slides again and finds empty headings; the handout repeats nothing but has the same empty headings, and the text version has both right.
  • “Do readers need the slides as they looked, with their layout and pictures?” Yes, the look matters: adds a caveat to Text version. The text version flows the words onto one A4 page without the slide design, so share it beside the handout rather than instead of it.

In short: the handout for readers who print or page through, the text version for listeners and for search, and the slides beside them for anyone who wants every step.

Checkpoint

Check: Which version to share

  1. Question 1 of 2 A participant who uses a screen reader asks for the talk. Which of the three PDFs from talk-touying-ua.typ suits them best?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The text version, export-mode=article

    Send the text version to listeners, and the handout to people who want the slides' look.

  2. Question 2 of 2 Someone wants to print the talk to take notes beside each slide. Which PDF?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The handout, one page per slide as it ended

    The handout is for paper and reading later; print it four to a sheet with a pages-per-sheet tool if needed.

Practise: fix a colour, read a veraPDF report

Two checks for a deck before you send it: the first darkens a theme colour step by step until it reaches a contrast ratio, the second reads veraPDF’s JSON report and lists the rules a PDF failed, as vera.py printed them. The sample tests of both steps run in the browser, and a first hint goes with each:

Code lab · 2 steps

Check a deck before you send it

Find the lightest darker shade of a colour that reaches a contrast ratio, then list the failed rules of a veraPDF report in the order of the standard.

  1. Darken a theme colour until it is readable To do
  2. List the rules a veraPDF report failed To do

Exercise · Medium · Python

Darken a theme colour until it is readable

A theme colour that fails a contrast check is usually fixed by making it darker, as little as possible, so that it still looks like the theme. Write darken_to(colour, background, target=4.5) in darken.py. Both colours are written as #RRGGBB (hexadecimal, any letter case). Return the first colour, written as #RRGGBB in upper case, that has a contrast ratio of at least target with background, trying the colour itself first and then darker steps.

  • Work out the contrast ratio with WCAG's formulas, as contrast.py in the lesson does: each channel divided by 255, then c / 12.92 when it is at most 0.04045, else ((c + 0.055) / 1.055) ** 2.4; the luminance is 0.2126 R + 0.7152 G + 0.0722 B; the ratio is (lighter + 0.05) / (darker + 0.05).
  • One darker step takes 1 away from each of the three channels that is still above 0.
  • If even black does not reach the target, return None.

Starter code · darken.py

def darken_to(colour, background, target=4.5):
    """Return the first colour, from colour itself down in steps of 1 per channel, that reaches the target ratio."""
    # Replace this line: work out the contrast ratio, then darken one step at a time.
    return colour
The sample tests · test_darken.py
from darken import darken_to


def test_a_colour_that_already_passes_is_kept():
    """Beamer's frame title blue, 9.27:1 on white"""
    assert darken_to("#3333B3", "#FFFFFF") == "#3333B3"
    assert darken_to("#3333b3", "#ffffff") == "#3333B3"


def test_touyings_grey_header():
    """#AAAAAA on white is 2.32:1; #767676 is the first grey from there that reaches 4.5:1"""
    assert darken_to("#AAAAAA", "#FFFFFF") == "#767676"


def test_the_target_can_change():
    """3:1 is enough for large text and for chart lines"""
    assert darken_to("#AAAAAA", "#FFFFFF", target=3) == "#949494"


def test_beamers_alert_red():
    """#FF0000 on white is 4.00:1"""
    assert darken_to("#FF0000", "#FFFFFF") == "#EE0000"


def test_metropolis_alert_on_its_background():
    assert darken_to("#EB811B", "#FAFAFA") == "#BE5400"


def test_black_is_the_last_step():
    """black on white is exactly 21:1, the highest ratio there is"""
    assert darken_to("#FF0000", "#FFFFFF", target=21) == "#000000"
    assert darken_to("#0000FF", "#000080") is None
A hint

Write a contrast(a, b) helper first and test it on the lesson's numbers: #FF0000 on #FFFFFF is 4.00. Then keep the three channels as integers in a loop: test the current colour, and if it fails, take 1 from every channel above 0 and try again, until all three are 0.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Medium · Python

List the rules a veraPDF report failed

verapdf --format json --flavour ua1 talk.pdf writes a JSON report; json.loads turns it into dictionaries and lists. Write failed_rules(report) in report.py. report is that parsed report. Return the failed rules of its first job as strings "<clause>-<test number>", such as "7.1-3", sorted in the order of the standard: by the numbers of the clause, then by the test number. Return an empty list for a compliant PDF.

  • The job is report["report"]["jobs"][0]. Its "validationResult" is a dictionary, or in some veraPDF versions a list whose first item is that dictionary.
  • The result's "details" holds "ruleSummaries", one dictionary per failed rule, with "clause" (a string such as "7.1" or "6.2") and "testNumber" (a number).
  • Sort clauses as numbers, part by part: "7.2" comes before "7.10", and "6.2" before both.

Starter code · report.py

def failed_rules(report):
    """Return the failed rules of a parsed veraPDF JSON report as "clause-test" strings, in the standard's order."""
    # Replace this line: find the first job's result, then sort its rule summaries.
    return []
The sample tests · test_report.py
from report import failed_rules


def rule(clause, test, checks=1):
    return {"ruleStatus": "FAILED", "clause": clause, "testNumber": test, "failedChecks": checks, "description": "…"}


def report(rules, as_list=False, compliant=False):
    result = {
        "profileName": "PDF/UA-1 validation profile",
        "compliant": compliant,
        "details": {"passedRules": 106 - len(rules), "failedRules": len(rules), "ruleSummaries": rules},
    }
    job = {"itemDetails": {"name": "talk-beamer.pdf"}, "validationResult": [result] if as_list else result}
    return {"report": {"jobs": [job]}}


# The seven rules the Beamer talk failed, in the order veraPDF happened to list them.
BEAMER = [rule("7.1", 3, 140), rule("6.2", 1), rule("7.2", 34, 131), rule("7.1", 11), rule("7.1", 8),
          rule("7.2", 2, 2), rule("7.1", 10)]


def test_the_beamer_talk():
    assert failed_rules(report(BEAMER)) == ["6.2-1", "7.1-3", "7.1-8", "7.1-10", "7.1-11", "7.2-2", "7.2-34"]


def test_a_list_of_results():
    assert failed_rules(report(BEAMER, as_list=True))[:2] == ["6.2-1", "7.1-3"]


def test_clauses_sort_as_numbers():
    assert failed_rules(report([rule("7.10", 1), rule("7.2", 5), rule("7.9", 2)])) == ["7.2-5", "7.9-2", "7.10-1"]


def test_a_compliant_pdf():
    assert failed_rules(report([], compliant=True)) == []
A hint

Get the result first, taking the first item when it is a list. Then sort the rule summaries with a key that turns the clause into a list of numbers and adds the test number: [int(n) for n in rule["clause"].split(".")], then rule["testNumber"].

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

The practice questions take the six ideas in turn and come back to any you miss:

Practice that adapts to you

Practise this lesson

18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 18 Which of these does a PDF/UA file need?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: Tagged content in a structure tree, a language and a title

    PDF/UA is about structure that software can follow; readability of colours is a separate check.

  2. Question 2 of 18 Rule 7.2-34, "natural language for text in page content shall be determined", failed in 131 checks. How is it fixed in a tagged LaTeX or Typst deck?

    Choose one answer.

    Show the answer: Question 2 of 18

    Answer: Declare the language, lang = en in \DocumentMetadata or #set text(lang: "en")

    A screen reader picks its pronunciation from the language the PDF declares.

  3. Question 3 of 18 A deck passes veraPDF with no failed rule. What else do you check before calling it accessible?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: Its structure tree, and ideally a listen with a screen reader, for repeated steps, empty headings and lost lines

    A validator answers whether the rules hold; the tree and a listen answer whether the deck works.

  4. Question 4 of 18 What must an ltx-talk source start with?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: \DocumentMetadata, before \documentclass

    The diff of the port changed exactly this: \DocumentMetadata and the class on the first two lines.

  5. Question 5 of 18 A checker warns that talk-ltxtalk.pdf jumps from H1 to H4. Why are the frame titles H4?

    Choose one answer.

    Show the answer: Question 5 of 18

    Answer: The class tags frame titles as H4 on purpose, well below sections and subsections, as its manual says

    The manual asks users of assistive technology for feedback on this choice; know it before a checker surprises you.

  6. Question 6 of 18 A frame shows a hint on slide 1 with \only<1>, then the answer on slide 2. Screen-reader users must hear both. What do you change?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: Keep the hint on the last slide too, with the answer added by \uncover<2>, or tag slide 1 as well with tag-slides

    Decide for each \only whether a listener needs its text; if so, it belongs on the last slide or in tag-slides.

  7. Question 7 of 18 According to Typst's accessibility guide, what does PDF/UA-1 export enforce?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: Tagged PDF, a document title, and descriptions for images and equations

    Set the title in set document, a description on every image and equation, and the language in set text.

  8. Question 8 of 18 A slide says "Roof area $A = 120 "m"^2$" in Typst and is exported as PDF/UA-1. What happens, and what are the two fixes?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: The export fails for the inline equation; give it a description with math.equation(alt …), or write the quantity as text

    Use maths where the content is maths, and plain text for quantities a sentence can say.

  9. Question 9 of 18 A screen-reader user says the headings of your Touying slides are empty. Which response follows from this lesson?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: Share the text version made with export-mode=article, whose H1 and H2 tags hold their words

    When the slides' tags fall short, give listeners the version whose tags are right.

  10. Question 10 of 18 What range can a WCAG contrast ratio take?

    Choose one answer.

    Show the answer: Question 10 of 18

    Answer: From 1:1 to 21:1

    The ratio is (L1 + 0.05) / (L2 + 0.05), with L1 the lighter colour's relative luminance.

  11. Question 11 of 18 WCAG lets large text reach 3:1 instead of 4.5:1. What counts as large?

    Choose one answer.

    Show the answer: Question 11 of 18

    Answer: At least 18 points, or 14 points and bold

    On slides, sizes change with the screen, so the lesson keeps all text at 4.5:1.

  12. Question 12 of 18 A slide chart shows two series as a red line and a green line, with a legend. What makes it readable for someone with a red-green deficiency?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: Label the lines directly, or give them different marks or dashes as well as colours

    Colour can add meaning but must never carry it alone; check with a colour-blindness simulator.

  13. Question 13 of 18 How large is the page of poster.typ and poster.tex?

    Choose one answer.

    Show the answer: Question 13 of 18

    Answer: A0, 841 by 1189 mm, which pdfinfo gave as 2383.94 by 3370.39 points

    Check the page size with pdfinfo before sending a poster to the printer.

  14. Question 14 of 18 How did poster.tex get 32-point body text from the article class, whose size options stop at 12pt?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: It redefined \normalsize, \large, \Large and \LARGE with \fontsize

    With unicode-math under LuaLaTeX, the fonts scale to any size, so the redefined sizes are exact.

  15. Question 15 of 18 A colleague likes a poster class that is not on the tagging status list at all. What do you do before using it for a tagged poster?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: Build a small poster with it and tagging on, then check it with veraPDF and read its structure tree

    The list says what has been tested; for the rest, your own build and check are the evidence.

  16. Question 16 of 18 Which command made the text version of the talk?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: typst compile --pdf-standard ua-1 --input export-mode=article talk-touying-ua.typ text.pdf

    One source, three outputs: slides, handout and text version, each from one flag.

  17. Question 17 of 18 Why does the text version of talk-touying-ua.typ include "The tank fills in the first rainy week." when Touying's handout of talk-touying.typ did not?

    Choose one answer.

    Show the answer: Question 17 of 18

    Answer: talk-touying-ua.typ keeps the line on the last step, while talk-touying.typ had it in #only(1)

    Write slides so that their last step holds every line a reader needs later.

  18. Question 18 of 18 After a talk, you will post the material on a course page for a mixed group of readers. What do you post?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: The handout and the text version, and the slides for anyone who wants every step

    Each version answers one kind of reader; one source makes them all.

PDF Accessibility Checker (PDF/UA) Open a built PDF in the browser and see its tags, language, title and image descriptions before a portal checks it.

Interview questions

Warm-up (fresher to 2–5 years): what does a PDF/UA check tell you about a deck, and what does it not tell you?

It tells you whether the PDF has the structure assistive software needs: a structure tree, content that is either tagged or marked as an artifact, a language, a title, descriptions for images and formulas. veraPDF tests those rules one by one; the Beamer talk failed 7 of the 106 in PDF/UA-1 because it had no tags at all. It does not tell you whether the tags make sense. The Typst talk passed all 106 rules while its heading tags were empty and every step of a list was tagged again, so a listener would hear the first question three times. Passing means the machine-checkable rules hold; reading the structure tree, and listening with a screen reader, tell you whether the deck works.

Two harder questions follow, about a conference that asks for tagged slides and a poster that is hard to read. Decide your first step before you open each one:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A conference asks every speaker for slides as a PDF/UA file, checked with veraPDF. Your 30-frame talk is in Beamer and fails 7 rules, among them the one that asks for a structure tree. You have a week. What do you do, and how do you show the result works for a listener as well as for the validator?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Your A0 poster's chart shows two series as a red and a green line, with a legend in light grey on white and the abstract at 24 points. A colleague with a colour-vision deficiency cannot tell the series apart, and others find the legend hard to read. What do you change, and how do you check each change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • veraPDF found no structure tree in the Beamer talk: 7 of the 106 PDF/UA-1 rules failed, and no option fixes it, because Beamer has no tagging support.
  • ltx-talk took the same frames with two changes and passed all 1,727 PDF/UA-2 rules; it tags the last slide of each frame, so a line on an early slide alone is never heard.
  • Typst exported the talk as PDF/UA-1 once both equations had descriptions; it passed 106 rules, but its tags repeat every step and leave the headings empty.
  • Theme colours fell as low as 2.32:1 against a bar of 4.5:1; darker colours and bold alerts fixed them, and colour must never be the only signal.
  • A poster is one tagged page whose columns are read in grid order; Typst and an article-class LaTeX page both passed, while beamerposter has no tagging support and tikzposter is not on the status list.
  • Share the handout and a text version beside the slides: the text version said each line once, with real headings.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What did veraPDF find in the Beamer talk?

    No structure tree: it failed 7 of the 106 PDF/UA-1 rules, and no theme or option can fix it, because Beamer has no tagging support.

  2. What is ltx-talk, and what does it need?

    A class with Beamer-like frames made for tagged output. It requires \DocumentMetadata, works best with LuaLaTeX and does not support XeTeX; the ported talk passed all 1,727 PDF/UA-2 rules.

  3. Which slide of a frame does ltx-talk tag?

    Only the last slide, by default; the others are artifacts. A line shown only on an early slide, such as with \only<1>, is never heard.

  4. Why did the Touying talk stop under --pdf-standard ua-1?

    Its display equations had no alt text. The error pointed inside Touying because the package rebuilds slide content; math.equation(alt: ...) on both equations fixed it.

  5. What contrast does WCAG 2.2 ask of slide text?

    At least 4.5:1 against its background; 3:1 for large text, meaning at least 18 point or 14 point bold, and for marks such as chart lines. Black text on white gives 21:1.

  6. Why make alerts bold as well as coloured?

    Colour must never be the only signal: a reader who cannot tell red from black, or a greyscale print, loses an emphasis that only its colour shows.

  7. How do you build an accessible A0 poster in LaTeX?

    Use the article class with geometry for the page, multicol and unicode-math, all marked compatible with tagging; beamerposter has no tagging support and tikzposter is not on the status list.

  8. Which version of a Typst talk suits a listener best?

    The text version, --input export-mode=article: one page that says each line once, with real H1 and H2 headings, beside the handout for readers who print.

Terms in this lesson

contrast ratio
WCAG's measure of how far apart two colours are in luminance, from 1:1 to 21:1, worked out as (L1 + 0.05) / (L2 + 0.05). Text needs at least 4.5:1, large text and graphics 3:1.
text version
A talk written out as a flowing document, with real headings and each line once, such as Touying's article mode gives, for readers who listen with a screen reader or search.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.