20
0

Merge the beamer-mako presentation template as slides_template

beamer-mako held a presentation template that was ported to the ltx-talk
class because beamer cannot produce tagged PDFs. That left the repository
misnamed and duplicating machinery cdsc_tex already has.

The ltx-talk branch was master plus fifteen commits, so this carries the
whole repository. Paths are rewritten under slides_template/; COPYING is
dropped in favour of the one here, which is the same licence.
This commit is contained in:
2026-08-19 15:38:28 +09:00
27 changed files with 1621 additions and 0 deletions

27
slides_template/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
/auto/
/vc
/*.aux
/*.log
/*.out
/*.pdf
/*.fdb_latexmk
/*.fls
/*.ttt
/*.nav
/*.toc
/*.snm
/*.xdv
/notes.config
*.aux
*.fdb_latexmk
*.fls
*.log
*.nav
*.notes
*.out
*.snm
*.toc
*.pdf
*.png
*-luamml-mathml.html
*.swp

38
slides_template/Makefile Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/make
# fonts.tex is a fragment meant to be \input, not a document of its own, so it
# has to stay out of the list or make will try to typeset it.
TEX := $(filter-out fonts.tex,$(wildcard *.tex))
PDF := $(patsubst %.tex,%.pdf,$(TEX))
JOB := $(patsubst %.tex,%,$(TEX))
all: slides
slides: vc $(PDF)
%.pdf: %.tex
latexmk -f -lualatex $<
# Double-width pages, slide left and notes right, for dspdfviewer and pdfpc.
# The notes pages are typeset with the same fonts as the slides.
notes: slides
./mknotes $(JOB)
vc:
resources/vc-git
viewpdf: slides
evince $(PDF)
pdfpc: notes
pdfpc -n right $(JOB)-notes.pdf
spell:
aspell -c -t --tex-check-comments -b $(TEX)
clean:
latexmk -C $(TEX)
rm -f *.notes *.tmp *.nav *.snm *.pdfpc *-luamml-mathml.html
rm -f vc
rm -f $(JOB)-notes.pdf
.PHONY: all slides notes vc viewpdf pdfpc spell clean

109
slides_template/README Normal file
View File

@@ -0,0 +1,109 @@
================================
=== Mako's Beamer Templates ====
================================
Copyright (c) 2013-2026 Benjamin Mako Hill <mako@atdot.cc> / <makohill@uw.edu>
This repository contains a Beamer presentation template I use for my
own presentations. Others in the CDSC lab also use it for lab
presentations. It is released under the GNU GPL version 3 or later.
The latest version can be found on GitLab at:
https://gitlab.com/makoshark/beamer-mako
You can clone the repository with:
git clone https://gitlab.com/makoshark/beamer-mako.git
Bug reports, comments, questions, and patches can be directed to:
Benjamin Mako Hill <makohill@uw.edu> [work]
Benjamin Mako Hill <mako@atdot.cc> [personal]
Patches can be submitted as merge requests via GitLab, or by cloning
the repository and sending patches by email:
git format-patch origin/master
git send-email *.patch
================================
=== Dependencies ===============
================================
On Debian or Ubuntu, install the following packages:
* latexmk
* texlive-luatex
* texlive-latex-recommended
* texlive-latex-extra
apt install latexmk texlive-luatex texlive-latex-recommended \
texlive-latex-extra
On other systems, installing a full TeX Live distribution should provide
everything needed.
The template uses the Metropolis Beamer theme, bundled in the template/
directory along with the OpenSans and RobotoMono fonts it requires.
================================
=== Accessibility ==============
================================
Slides are built as tagged PDFs that validate against PDF/UA-2
(ISO 14289-2), which is what upload targets like Canvas check for. Check
a file with veraPDF (https://verapdf.org/):
verapdf -f ua2 my-talk.pdf
The pieces that do this work are:
* \DocumentMetadata declares PDF/UA-2, PDF 2.0, and the document
language. The language tag tells a screen reader how to pronounce
the text.
* tagpdf with activate-all turns tagging on.
* The frametitle template is patched to emit H1 tags, since Beamer
does not treat frame titles as headings on its own.
* unicode-math replaces the Computer Modern math fonts, which carry no
Unicode mappings and leave formulas unreadable to assistive software.
* \hypersetup{pdfdisplaydoctitle=true} makes viewers announce the
document title instead of the filename.
* The tikz overlays behind \credit, \marktopleft, and \markbottomright
are marked as artifacts so they stay out of the reading order.
Use a slides-only build (make slides) for anything shared. Builds that
include speaker notes do not validate, because Beamer repeats the frame
title and section headings on the notes half of each page.
Validating is not the same as being accessible, and this template does
the first better than the second. Frame titles and links are tagged;
body text, lists, and figures are not, so a screen reader gets the
outline of a talk rather than its contents. Closing that gap needs
automatic tagging from the LaTeX kernel (\DocumentMetadata{tagging=on}),
which requires TeX Live 2025 or later and is waiting on Overleaf to
support it, and Beamer support in the LaTeX Tagging Project, which does
not exist yet. The alt={...} arguments in the template are written for
that day; they are dropped by the current toolchain.
================================
=== Scripts ====================
================================
new_beamer_presentation
-----------------------
Creates a new presentation directory from the template. Takes a
destination directory as its argument:
new_beamer_presentation <directory>
Example:
new_beamer_presentation my-talk
This creates my-talk/ populated with the template files, with the main
source file renamed to my-talk.tex. Fonts are symlinked from the source
repository rather than copied, so the repository must remain in place.
Symlink the script into your PATH with:
ln -s ~/tex/beamer-mako/new_beamer_presentation ~/bin/

View File

@@ -0,0 +1,97 @@
# ltx-talk port
This branch is the presentation template ported from beamer to the
[ltx-talk](https://github.com/josephwright/ltx-talk) class. ltx-talk was
written with tagging as a design goal.
## Requirements
* LuaLaTeX
* TeX Live 2025 or newer (ltx-talk needs LaTeX 2025-11-01 or later);
`texlive-latex-recommended` ships the class
* `python3-pypdf`, for assembling the notes PDF
You can install all three on Debian with:
```
apt install texlive-latex-recommended python3-pypdf
```
## Building
```
make slides # the deck; this is what gets uploaded
make notes # double-width slide + notes pages for presenting
make pdfpc # build the notes PDF and open it in pdfpc
```
Share the slides build, not the notes build: only the slides are tagged and
expected to validate with tools like [veraPDF](https://verapdf.org/).
## Migrating from beamer
| beamer | here |
|---|---|
| `\begin{frame}{Title}` | `\frametitle{Title}` inside the frame |
| `\begin{frame}[standout]{}` | `\standout{...}` |
| `\sectionpage` from the theme | `\section{...}` then `\sectionpage` |
| `\subsectionpage` | `\subsection{...}` then `\subsectionpage` |
| `\note{...}`, `\note[item]{...}` | same, after `\usepackage{talk-notes}` |
| `\appendix` | same |
| `\alert{...}`, `\item<2->`, `\pause` | same |
| `\only<1>{...}` | avoid; use `\pause` or `\begin{itemize}[<+->]` |
Use `\frametitle`, not the braced argument: by default the class treats
`\begin{frame}{Title}` as body text, so the title is not tagged as a heading.
Give every `\includegraphics` an `alt={...}` description, and declare table
header rows with `\tagpdfsetup{table/header-rows={1}}` before the tabular.
## Speaker notes
ltx-talk has no `\note` command (see
[issue #156](https://github.com/josephwright/ltx-talk/issues/156)).
`talk-notes.sty` defines one that records its text to a side file and
contributes nothing to the slides; `mknotes` reads that file afterwards and
builds the presenter PDF. Present it with
[dspdfviewer](https://github.com/dannyedel/dspdfviewer),
[pdfpc](https://pdfpc.github.io/), or the web viewers
[Beamer Viewer](https://beamerviewer.euxane.eu/) and
[backstage](https://bckstg.xyz/).
### Installing speaker notes
To use speaker notes in another ltx-talk document, copy two files out of this
repository:
- `mknotes`
- `talk-notes.sty`
Load the package in the preamble:
```latex
\usepackage{talk-notes}
```
Write notes with `\note{...}`, or `\note<2->{...}` to bind one to particular
slides of a frame. As in beamer, plain notes run together as text, each
starting a new paragraph, and `\note[item]{...}` adds an entry to a numbered
list printed after them.
Build the deck, then assemble the presenter PDF:
```
./mknotes example
```
That reads `example.pdf` and `example.notes` and writes `example-notes.pdf`,
the double-width version you present from. The slides in `example.pdf` are
untouched.
If `fonts.tex` is present it is used for the notes pages, so they match the
deck. `--preamble FILE` names a different fragment.
## Theme
`trantor.sty` is [Marco Pompili's](https://github.com/mrc-pop)
[Metropolis-like theme](https://github.com/mrc-pop/trantor) for
ltx-talk.

123
slides_template/example.tex Normal file
View File

@@ -0,0 +1,123 @@
\DocumentMetadata{
tagging=on,
tagging-setup={math/setup=mathml-SE},
pdfstandard=ua-2,
pdfversion=2.0,
lang=en-US,
}
\documentclass[aspectratio=169]{ltx-talk}
\usepackage{unicode-math}
\usepackage{graphicx}
\usepackage{booktabs}
\input{fonts.tex}
\definecolor{makopurple1}{RGB}{57,39,91}
\definecolor{makopurple4}{RGB}{124,99,173}
\definecolor{accent}{RGB}{124,99,173}
\colorlet{TrantorDark}{makopurple1}
\usepackage{trantor}
\usepackage{talk-notes}
\hypersetup{pdfdisplaydoctitle=true,pdftitle={Presentation Title}}
\renewcommand{\titlelogo}{%
\includegraphics[height=1.4cm,
alt={Community Data Science Collective logo}]{figures/logo.pdf}}
% \e{} was the beamer template's emphasis command; ltx-talk provides \alert
\newcommand{\e}[1]{\alert{#1}}
\title{Presentation Title}
\subtitle{Presentation Subtitle}
\author{\textbf{Benjamin Mako Hill}\\
makohill@uw.edu}
\institute{\textbf{University of Washington}\\
Department of Communication\\
\emph{Associate Professor}\\
\hspace{1pt}\\
\textbf{Harvard University}\\
Berkman Klein Center\\
\emph{Faculty Associate}}
\date{December 2, 1980}
\begin{document}
\maketitle
\section{Introduction}
\sectionpage
\begin{frame}
\frametitle{A sample slide}
A displayed formula:
\[ \int_{-\infty}^\infty e^{-x^2} \, dx = \sqrt{\pi} \]
An itemized list:
\begin{itemize}[<+->]
\item itemized item 1
\item itemized item 2
\item itemized item 3
\end{itemize}
\footline{Image credit: Shaw and Hill, 2099.}
\note<1>{Set up the integral before revealing anything else.}
\note{Mention that this is the Gaussian integral.}
\note<3->{Once the whole list is up, tie it back to the opening claim.}
\note[item]{Check the clock here.}
\note[item]{Ask whether anyone has seen the 2099 preprint.}
\end{frame}
\begin{frame}
\frametitle{Short frame title}
\e{A first} paragraph with \texttt{monospace text}.
And then \e{a second} paragraph.
\note{A note that applies to this whole frame.}
\end{frame}
\subsection{An Introductory Subsection}
\subsectionpage
\begin{frame}
\frametitle{After the subsection page}
Subsections get their own divider, showing the section above the subsection.
\note{The subsection page is trantor's, not ltx-talk's.}
\end{frame}
\appendix
\standout{Example and Data Appendix}
\begin{frame}
\frametitle{A table}
% without this the header cells are tagged TD like any other
\tagpdfsetup{table/header-rows={1}}
\begin{tabular}{lrr}
\toprule
Wiki & Editors & Articles \\
\midrule
English & 121{,}000 & 6{,}900{,}000 \\
German & 17{,}000 & 2{,}900{,}000 \\
Cebuano & 150 & 6{,}100{,}000 \\
\bottomrule
\end{tabular}
\note{Numbers are made up; the point is that the table is tagged.}
\end{frame}
\begin{frame}
\frametitle{A figure}
\includegraphics[width=0.3\linewidth,
alt={Community Data Science Collective logo}]{figures/logo.pdf}
Every figure needs alt text, or the artifact option if it is decoration.
\note{Alt text reaches the PDF here, unlike under beamer.}
\end{frame}
\end{document}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
xml:space="preserve"
width="1172.3047"
height="783.17554"
viewBox="0 0 1172.3047 783.17554"
sodipodi:docname="CDSC_logo-notext.svg"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs6" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1276"
inkscape:window-height="1417"
id="namedview4"
showgrid="false"
inkscape:zoom="1.3513094"
inkscape:cx="466.63864"
inkscape:cy="391.58777"
inkscape:window-x="0"
inkscape:window-y="19"
inkscape:window-maximized="0"
inkscape:current-layer="g10" /><g
id="g10"
inkscape:groupmode="layer"
inkscape:label="logo-nowords"
transform="matrix(1.25,0,0,-1.25,0,783.17551)"><g
id="g12"><path
d="m 264.555,134.212 c 21.789,-5.172 44.433,-9.539 67.789,-13.039 l 0,-117.633 68.058,110.121 c 22.395,-1.621 45.266,-2.476 68.52,-2.476 14.801,0 29.433,0.359 43.898,1.023 l 0,-108.668 70.844,114.633 c 38.504,4.774 75.234,11.922 109.633,21.094 l 0,-118.07 92.68,149.961 0,0.168 c 13.992,6.128 27.339,12.64 39.937,19.531 6.746,3.691 13.285,7.484 19.602,11.379 53.511,35.312 88.832,95.961 88.832,164.871 0,68.914 -35.321,129.566 -88.832,164.875 -6.321,3.894 -12.856,7.691 -19.602,11.379 -12.598,6.89 -25.945,13.402 -39.937,19.531 l 0,0.168 -0.379,0 c -85.61,37.418 -196.067,59.973 -316.676,59.973 -120.61,0 -231.063,-22.555 -316.672,-59.973 l -0.379,0 0,-0.168 c -13.988,-6.129 -27.34,-12.637 -39.933,-19.527 -6.75,-3.692 -13.293,-7.493 -19.614,-11.387 C 38.816,496.665 3.5,436.017 3.5,367.107 3.5,298.201 38.812,237.552 92.324,202.24 c 6.321,-3.895 12.864,-7.696 19.614,-11.387 3.597,-1.969 7.253,-3.906 10.968,-5.809 9.301,-4.769 18.969,-9.34 28.965,-13.718 l 0,-150.129 75.859,122.75"
style="fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path14"
inkscape:connector-curvature="0" /><path
d="m 349.824,341.501 22.086,-93.433 102.012,138.648 35.027,-35.027 81.992,163.992 41.36,-102.418 45.058,13.957 31.301,-69.973 26.992,20.653"
style="fill:none;stroke:#463470;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path16"
inkscape:connector-curvature="0" /><path
d="m 151.871,21.201 197.953,320.3"
style="fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path18"
inkscape:connector-curvature="0" /><path
d="m 228.441,476.017 0,-258.023"
style="fill:none;stroke:#b3b3b3;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path20"
inkscape:connector-curvature="0" /><path
d="m 294.793,217.99 389.691,0"
style="fill:none;stroke:#b3b3b3;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path22"
inkscape:connector-curvature="0" /><path
d="m 228.441,217.99 25.254,0"
style="fill:none;stroke:#b3b3b3;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path24"
inkscape:connector-curvature="0" /></g></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

19
slides_template/fonts.tex Normal file
View File

@@ -0,0 +1,19 @@
% Font setup shared by the slides and by the notes pages that mknotes
% typesets, so that the two match. Loaded by example.tex directly and passed
% to mknotes with --preamble.
%
% Requires fontspec, which unicode-math pulls in on the slides side. Paths must
% stay relative to the document directory: LuaTeX fails to resolve a
% parent-relative Path= and dies with an unhelpful empty-filename error.
\setsansfont{OpenSans}[
Path = fonts/OpenSans/, Extension = .ttf,
UprightFont = *-Regular, BoldFont = *-Bold,
ItalicFont = *-Italic, BoldItalicFont = *-BoldItalic]
\setmonofont{RobotoMono}[
Path = fonts/RobotoMono/, Extension = .ttf,
UprightFont = *-Regular, BoldFont = *-Bold,
ItalicFont = *-Italic, BoldItalicFont = *-BoldItalic]
\renewcommand{\familydefault}{\sfdefault}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

384
slides_template/mknotes Executable file
View File

@@ -0,0 +1,384 @@
#!/usr/bin/env python3
"""Build a presenter PDF from a slides PDF and the notes captured beside it.
The ltx-talk class has no speaker notes, so talk-notes.sty records them to a
side file while the slides build ignores them. This program reads that file and
reassembles the two halves.
The reason a separate program is needed at all is that a frame is not a page:
overlays expand one frame into several slides, and neither the source nor a
reader of the finished PDF can work out which page belongs to which frame.
talk-notes.sty resolves that by writing a record per page at shipout time, so
the mapping here is exact rather than inferred.
The output is double-width pages, slide on the left and notes on the right,
which is the format dspdfviewer and pdfpc expect. The notes half carries a grey
header band with the section and position in the talk, a thumbnail of the
current slide flush into its outer corner, and the notes below. Previous and
next slides are left to the viewer, which already shows them.
The output is not tagged and is not meant to be shared: it exists to be looked
at while talking. The slides build is the one that gets uploaded.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pypdf import PageObject, PdfReader, PdfWriter, Transformation
# ---------------------------------------------------------------- parsing
class Page:
"""One shipped page, and where it sits in the talk."""
def __init__(self, number, frame, slide):
self.number = number
self.frame = frame
self.slide = slide
self.section = ""
self.subsection = ""
self.notes = []
self.items = []
def parse_notes_file(path):
"""Return (pages, notes_by_frame) from a .notes file."""
pages = []
notes = {}
seen = set()
with open(path, encoding="utf-8") as handle:
lines = handle.read().split("\n")
i = 0
while i < len(lines):
line = lines[i].strip()
match = re.match(r"^PAGE (\d+)\|FRAME (\d+)\|SLIDE (\d+)$", line)
if match:
pages.append(Page(*(int(g) for g in match.groups())))
i += 1
continue
if line.startswith("SECTION ") or line == "SECTION":
if pages:
pages[-1].section = line[8:].strip()
i += 1
continue
if line.startswith("SUBSECTION ") or line == "SUBSECTION":
if pages:
pages[-1].subsection = line[11:].strip()
i += 1
continue
match = re.match(r"^NOTE FRAME (\d+)\|SPEC (.*)\|KIND (.*)$", line)
if match:
frame = int(match.group(1))
spec, kind = match.group(2).strip(), match.group(3).strip()
body = []
i += 1
while i < len(lines) and lines[i].strip() != "ENDNOTE":
text = lines[i]
body.append(text[5:] if text.startswith("TEXT ") else text)
i += 1
i += 1
text = " ".join(part.strip() for part in body).strip()
# A note is re-run for every slide of its frame, so the same record
# arrives once per slide. Keep the first occurrence only.
key = (frame, spec, kind, text)
if key not in seen:
seen.add(key)
notes.setdefault(frame, []).append((spec, kind, text))
continue
i += 1
return pages, notes
def spec_matches(spec, slide, last):
"""Does an overlay spec such as '2-', '-3', '1,4' cover this slide?
A bare '-' means the note was given without a spec and applies throughout.
'n' inside a range means the frame's last slide, as in ltx-talk.
"""
if spec in ("", "-"):
return True
def value(token, default):
token = token.strip()
if token == "":
return default
if token == "n":
return last
try:
return int(token)
except ValueError:
return default
for part in spec.split(","):
part = part.strip()
if not part:
continue
if "-" in part:
low, _, high = part.partition("-")
if value(low, 1) <= slide <= value(high, last):
return True
elif value(part, -1) == slide:
return True
return False
def attach_notes(pages, notes):
"""Work out which notes belong on each page."""
last_slide = {}
for page in pages:
last_slide[page.frame] = max(last_slide.get(page.frame, 0), page.slide)
for page in pages:
for spec, kind, text in notes.get(page.frame, []):
if spec_matches(spec, page.slide, last_slide[page.frame]):
if kind == "item":
page.items.append(text)
else:
page.notes.append(text)
# ---------------------------------------------------------------- typesetting
#: Side margin on the generated notes pages, as a fraction of the page.
HMARGIN = 0.06
#: Font setup shared with the slides. Used when it exists, so that the notes
#: match the deck without anyone having to remember a flag.
DEFAULT_PREAMBLE = "fonts.tex"
# The page is laid out by hand rather than by geometry: the header band runs
# full bleed to the top and outer edges to meet the thumbnail, so the margins
# have to be applied per element instead of to the page.
NOTES_PREAMBLE = r"""\documentclass{article}
\usepackage[paperwidth=%(width).2fbp,paperheight=%(height).2fbp,
margin=0bp]{geometry}
\usepackage{parskip}
\usepackage{xcolor}
\usepackage{fontspec}
\definecolor{notegrey}{RGB}{230,230,230}
\pagestyle{empty}
\setlength{\parindent}{0pt}
\setlength{\fboxsep}{0pt}
\setlength{\topskip}{0pt}
%(preamble)s
\begin{document}
"""
def escape_for_context(text):
"""Guard the few characters that would derail a generated file.
Note bodies are written out by talk-notes.sty as literal source, so LaTeX
markup inside a note is deliberately preserved. Only a bare percent sign is
dangerous, since it would comment out the rest of the generated line.
"""
return re.sub(r"(?<!\\)%", r"\\%", text)
def build_notes_pdf(pages, width, height, thumb_w, thumb_h, workdir,
preamble=None, jobname="notes"):
"""Typeset one notes page per slide page and return the resulting PDF path.
The page is made at the size it will actually occupy in the finished sheet,
so that it can be dropped in without scaling. Shrinking a slide-sized page
into a narrow column would leave the text far too small to read while
presenting.
thumb_w and thumb_h are the size of the slide thumbnail that assembly stamps
flush into the top outer corner. The header band is made exactly as tall as
the thumbnail and stops exactly where it begins, so the two read as a single
band across the head of the page.
"""
hmargin = width * HMARGIN
pad = width * 0.03
gap = height * 0.05
textwidth = width - hmargin * 2
bandwidth = width - thumb_w
# \input rather than the file's contents, so that anything relative inside
# it (font paths, say) resolves against the document directory. LaTeX runs
# from there even though its output goes to the working directory.
extra = r"\input{%s}" % preamble if preamble else ""
body = [NOTES_PREAMBLE % {
"width": width, "height": height, "preamble": extra,
}]
total = len(pages)
for index, page in enumerate(pages):
where = page.section or ""
if page.subsection:
where = f"{where} / {page.subsection}" if where else page.subsection
# Sans-serif and small, against the roman body below, so the heading
# reads as apparatus rather than as part of the notes.
heading = [r"\sffamily\small"]
if where:
heading.append(r"{\bfseries %s\par}" % escape_for_context(where))
heading.append(
r"{\footnotesize Frame %d, slide %d of %d \quad Page %d of %d\par}"
% (page.frame, page.slide,
max(p.slide for p in pages if p.frame == page.frame),
page.number, total)
)
inner = (r"\hspace{%.2fbp}\begin{minipage}[t]{%.2fbp}\vspace{%.2fbp}%s"
r"\end{minipage}"
% (pad, bandwidth - pad * 2, pad, "".join(heading)))
if thumb_h:
band = (r"\begin{minipage}[t][%.2fbp][t]{%.2fbp}%s\end{minipage}"
% (thumb_h, bandwidth, inner))
else:
band = (r"\begin{minipage}[t]{%.2fbp}%s\vspace{%.2fbp}\end{minipage}"
% (bandwidth, inner, pad))
body.append(r"\noindent\colorbox{notegrey}{%s}%%" % band)
body.append(r"\par\vspace{%.2fbp}%%" % gap)
# Paragraph spacing is set here rather than in the preamble so that it
# separates the notes without also loosening the heading band above.
body.append(r"\noindent\hspace{%.2fbp}\begin{minipage}[t]{%.2fbp}"
r"\setlength{\parskip}{0.7em}"
% (hmargin, textwidth))
# As in beamer, plain \note commands run together as text and
# \note[item] ones become a numbered list printed after it. beamer
# concatenates the plain ones with no separator at all; a paragraph
# break between them is easier to read and is the one deviation.
if page.notes:
body.append("\n\n".join(
escape_for_context(n) for n in page.notes))
if page.items:
body.append(r"\par\begin{enumerate}\setlength{\itemsep}{0pt}"
r"\setlength{\parskip}{0pt}"
r"\setlength{\leftmargini}{1.4em}")
for item in page.items:
body.append(r"\item %s" % escape_for_context(item))
body.append(r"\end{enumerate}")
body.append(r"\end{minipage}")
if index != total - 1:
body.append(r"\newpage")
body.append(r"\end{document}")
source = os.path.join(workdir, jobname + ".tex")
with open(source, "w", encoding="utf-8") as handle:
handle.write("\n".join(body) + "\n")
# Run from the document directory so relative paths in the preamble resolve,
# but send the output elsewhere so the build leaves nothing behind.
result = subprocess.run(
["lualatex", "-interaction=nonstopmode", "-halt-on-error",
"-output-directory=" + workdir, source],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
output = os.path.join(workdir, jobname + ".pdf")
if result.returncode != 0 or not os.path.exists(output):
sys.stderr.write(result.stdout.decode("utf-8", "replace")[-3000:])
raise SystemExit("mknotes: failed to typeset the notes pages")
return output
# ---------------------------------------------------------------- assembly
def place(target, source, scale, x, y):
"""Draw source onto target, scaled, with its lower-left corner at (x, y)."""
target.merge_transformed_page(
source, Transformation().scale(scale).translate(x, y)
)
def assemble_notes(slides, notes_pdf, width, height, thumb_scale):
"""Double-width pages: slide left, thumbnail and notes right."""
writer = PdfWriter()
for index, note_page in enumerate(notes_pdf.pages):
sheet = PageObject.create_blank_page(width=width * 2, height=height)
place(sheet, slides.pages[index], 1.0, 0, 0)
place(sheet, note_page, 1.0, width, 0)
if thumb_scale:
# Flush into the top outer corner of the notes half.
tw, th = width * thumb_scale, height * thumb_scale
place(sheet, slides.pages[index], thumb_scale,
width * 2 - tw, height - th)
writer.add_page(sheet)
return writer
# ---------------------------------------------------------------- entry point
def main():
parser = argparse.ArgumentParser(
description="Assemble a presenter PDF from slides and captured notes.")
parser.add_argument("jobname",
help="base name of the build, e.g. 'example'")
parser.add_argument("--output",
help="output file (default: <jobname>-notes.pdf)")
parser.add_argument("--preamble", metavar="FILE", default=DEFAULT_PREAMBLE,
help="LaTeX fragment to \\input into the notes pages, "
"so they can share the slides' font setup "
f"(default: {DEFAULT_PREAMBLE} when it exists)")
parser.add_argument("--thumb-scale", type=float, default=0.29,
metavar="F",
help="slide thumbnail size on the notes half, as a "
"fraction of the slide (default: 0.29; 0 disables)")
args = parser.parse_args()
slides_path = args.jobname + ".pdf"
notes_path = args.jobname + ".notes"
for path in (slides_path, notes_path):
if not os.path.exists(path):
raise SystemExit(f"mknotes: {path} not found; build the slides first")
pages, notes = parse_notes_file(notes_path)
if not pages:
raise SystemExit(f"mknotes: no page records in {notes_path}")
attach_notes(pages, notes)
slides = PdfReader(slides_path)
if len(slides.pages) != len(pages):
raise SystemExit(
f"mknotes: {slides_path} has {len(slides.pages)} pages but "
f"{notes_path} describes {len(pages)}; rebuild the slides")
box = slides.pages[0].mediabox
width, height = float(box.width), float(box.height)
thumb = max(0.0, min(args.thumb_scale, 0.9))
# A preamble named on the command line has to be there; the default one is
# a convenience and is skipped when the document does not use it.
preamble = args.preamble
if preamble and not os.path.exists(preamble):
if preamble != DEFAULT_PREAMBLE:
raise SystemExit(f"mknotes: {preamble} not found")
preamble = None
workdir = tempfile.mkdtemp(prefix="mknotes-")
try:
notes_pdf = PdfReader(build_notes_pdf(
pages, width, height, width * thumb, height * thumb, workdir,
preamble=preamble))
writer = assemble_notes(slides, notes_pdf, width, height, thumb)
output = args.output or f"{args.jobname}-notes.pdf"
with open(output, "wb") as handle:
writer.write(handle)
print(f"mknotes: wrote {output} ({len(writer.pages)} pages)")
finally:
shutil.rmtree(workdir, ignore_errors=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,43 @@
#!/bin/sh
# Helper script to export LaTeX templates for new documents
# Copyright (c) 2009-2022 Benjamin Mako Hill <mako@atdot.cc>
# Released under the GPLv3 or later.
set -e
SOURCEDIR="$HOME/tex/beamer-mako"
CURDIR=$(pwd)
if [ -n "$1" ]
then
if [ -e "$1" ]; then
echo "error: '$1' already exists"
exit 1
fi
WORK_DIR=$(mktemp -d)
cd "$SOURCEDIR"
git archive --format=tar HEAD template | tar x --strip=1 -C "$WORK_DIR"
cd "$CURDIR"
mv "$WORK_DIR" "$1"
mv "$1/example.tex" "$1/$(basename "$1").tex"
# create a symlink farm for the fonts
cd "$1"
rm -rf ./fonts/*
# first create the directories
for fontdir in "${SOURCEDIR}/template/fonts/"*; do
mkdir "./fonts/$(basename "${fontdir}")"
# then the files
for fontfile in "${fontdir}/"*; do
ln -s "${fontfile}" "./fonts/$(basename "${fontdir}")/$(basename "${fontfile}")"
done
done
cd "$CURDIR"
else
echo "specify a directory where the template should go"
fi

View File

@@ -0,0 +1,123 @@
%%
%% This is file `pgfplotsthemetol.sty',
%% generated with the docstrip utility.
%%
%% The original source files were:
%%
%% pgfplotsthemetol.dtx (with options: `package')
%% ---------------------------------------------------------------------------
%% Copyright 2015 Matthias Vogelgesang and the LaTeX community. A full list of
%% contributors can be found at
%%
%% https://github.com/matze/mtheme/graphs/contributors
%%
%% and the original template was based on the HSRM theme by Benjamin Weiss.
%%
%% This work is licensed under a Creative Commons Attribution-ShareAlike 4.0
%% International License (https://creativecommons.org/licenses/by-sa/4.0/).
%% ---------------------------------------------------------------------------
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{pgfplotsthemetol}
[2017/01/23 PGFplots colors based on Paul Tol's SRON technical note]
\definecolor{TolDarkPurple}{HTML}{332288}
\definecolor{TolDarkBlue}{HTML}{6699CC}
\definecolor{TolLightBlue}{HTML}{88CCEE}
\definecolor{TolLightGreen}{HTML}{44AA99}
\definecolor{TolDarkGreen}{HTML}{117733}
\definecolor{TolDarkBrown}{HTML}{999933}
\definecolor{TolLightBrown}{HTML}{DDCC77}
\definecolor{TolDarkRed}{HTML}{661100}
\definecolor{TolLightRed}{HTML}{CC6677}
\definecolor{TolLightPink}{HTML}{AA4466}
\definecolor{TolDarkPink}{HTML}{882255}
\definecolor{TolLightPurple}{HTML}{AA4499}
\pgfplotscreateplotcyclelist{mbarplot cycle}{%
{draw=TolDarkBlue, fill=TolDarkBlue!70},
{draw=TolLightBrown, fill=TolLightBrown!70},
{draw=TolLightGreen, fill=TolLightGreen!70},
{draw=TolDarkPink, fill=TolDarkPink!70},
{draw=TolDarkPurple, fill=TolDarkPurple!70},
{draw=TolDarkRed, fill=TolDarkRed!70},
{draw=TolDarkBrown, fill=TolDarkBrown!70},
{draw=TolLightRed, fill=TolLightRed!70},
{draw=TolLightPink, fill=TolLightPink!70},
{draw=TolLightPurple, fill=TolLightPurple!70},
{draw=TolLightBlue, fill=TolLightBlue!70},
{draw=TolDarkGreen, fill=TolDarkGreen!70},
}
\pgfplotscreateplotcyclelist{mlineplot cycle}{%
{TolDarkBlue, mark=*, mark size=1.5pt},
{TolLightBrown, mark=square*, mark size=1.3pt},
{TolLightGreen, mark=triangle*, mark size=1.5pt},
{TolDarkBrown, mark=diamond*, mark size=1.5pt},
}
\pgfplotsset{
compat=1.9,
mlineplot/.style={
mbaseplot,
xmajorgrids=true,
ymajorgrids=true,
major grid style={dotted},
axis x line=bottom,
axis y line=left,
legend style={
cells={anchor=west},
draw=none
},
cycle list name=mlineplot cycle,
},
mbarplot base/.style={
mbaseplot,
bar width=6pt,
axis y line*=none,
},
mbarplot/.style={
mbarplot base,
ybar,
xmajorgrids=false,
ymajorgrids=true,
area legend,
legend image code/.code={%
\draw[#1] (0cm,-0.1cm) rectangle (0.15cm,0.1cm);
},
cycle list name=mbarplot cycle,
},
horizontal mbarplot/.style={
mbarplot base,
xmajorgrids=true,
ymajorgrids=false,
xbar stacked,
area legend,
legend image code/.code={%
\draw[#1] (0cm,-0.1cm) rectangle (0.15cm,0.1cm);
},
cycle list name=mbarplot cycle,
},
mbaseplot/.style={
legend style={
draw=none,
fill=none,
cells={anchor=west},
},
x tick label style={
font=\footnotesize
},
y tick label style={
font=\footnotesize
},
legend style={
font=\footnotesize
},
major grid style={
dotted,
},
axis x line*=bottom,
},
disable thousands separator/.style={
/pgf/number format/.cd,
1000 sep={}
},
}
\endinput
%%
%% End of file `pgfplotsthemetol.sty'.

View File

@@ -0,0 +1,24 @@
#!/bin/sh
# This is file 'vc' from the vc bundle for TeX.
# The original file can be found at CTAN:support/vc.
# This file is Public Domain.
# Parse command line options.
full=0
mod=0
while [ -n "$(echo $1 | grep '-')" ]; do
case $1 in
-f ) full=1 ;;
-m ) mod=1 ;;
* ) echo 'usage: vc [-f] [-m]'
exit 1
esac
shift
done
# English locale.
LC_ALL=C
git --no-pager log -1 HEAD --pretty=format:"Hash: %H%nAbr. Hash: %h%nParent Hashes: %P%nAbr. Parent Hashes: %p%nAuthor Name: %an%nAuthor Email: %ae%nAuthor Date: %ai%nCommitter Name: %cn%nCommitter Email: %ce%nCommitter Date: %ci%n" |gawk -v script=log -v full=$full -f ./resources/vc-git.awk > vc
if [ "$mod" = 1 ]
then
git status |gawk -v script=status -f ~/bin/vc-git.awk >> vc
fi

View File

@@ -0,0 +1,89 @@
# This is file 'vc-git.awk' from the vc bundle for TeX.
# The original file can be found at CTAN:support/vc.
# This file is Public Domain.
BEGIN {
### Process output of "git status".
if (script=="status") {
modified = 0
}
}
### Process output of "git log".
script=="log" && /^Hash:/ { Hash = substr($0, 2+match($0, ":")) }
script=="log" && /^Abr. Hash:/ { AbrHash = substr($0, 2+match($0, ":")) }
script=="log" && /^Parent Hashes:/ { ParentHashes = substr($0, 2+match($0, ":")) }
script=="log" && /^Abr. Parent Hashes:/ { AbrParentHashes = substr($0, 2+match($0, ":")) }
script=="log" && /^Author Name:/ { AuthorName = substr($0, 2+match($0, ":")) }
script=="log" && /^Author Email:/ { AuthorEmail = substr($0, 2+match($0, ":")) }
script=="log" && /^Author Date:/ { AuthorDate = substr($0, 2+match($0, ":")) }
script=="log" && /^Committer Name:/ { CommitterName = substr($0, 2+match($0, ":")) }
script=="log" && /^Committer Email:/ { CommitterEmail = substr($0, 2+match($0, ":")) }
script=="log" && /^Committer Date:/ { CommitterDate = substr($0, 2+match($0, ":")) }
### Process output of "git status".
### Changed index?
script=="status" && /^# Changes to be committed:/ { modified = 1 }
### Unstaged modifications?
script=="status" && /^# Changed but not updated:/ { modified = 2 }
END {
### Process output of "git log".
if (script=="log") {
### Standard encoding is UTF-8.
if (Encoding == "") Encoding = "UTF-8"
### Extract relevant information from variables.
LongDate = substr(AuthorDate, 1, 25)
DateRAW = substr(LongDate, 1, 10)
DateISO = DateRAW
DateTEX = DateISO
gsub("-", "/", DateTEX)
Time = substr(LongDate, 12, 14)
### Write file identification to vc.tex.
print "%%% This file has been generated by the vc bundle for TeX."
print "%%% Do not edit this file!"
print "%%%"
### Write Git specific macros.
print "%%% Define Git specific macros."
print "\\gdef\\GITHash{" Hash "}%"
print "\\gdef\\GITAbrHash{" AbrHash "}%"
print "\\gdef\\GITParentHashes{" ParentHashes "}%"
print "\\gdef\\GITAbrParentHashes{" AbrParentHashes "}%"
print "\\gdef\\GITAuthorName{" AuthorName "}%"
print "\\gdef\\GITAuthorEmail{" AuthorEmail "}%"
print "\\gdef\\GITAuthorDate{" AuthorDate "}%"
print "\\gdef\\GITCommitterName{" CommitterName "}%"
print "\\gdef\\GITCommitterEmail{" CommitterEmail "}%"
print "\\gdef\\GITCommitterDate{" CommitterDate "}%"
### Write generic version control macros.
print "%%% Define generic version control macros."
print "\\gdef\\VCRevision{\\GITAbrHash}%"
print "\\gdef\\VCAuthor{\\GITAuthorName}%"
print "\\gdef\\VCDateRAW{" DateRAW "}%"
print "\\gdef\\VCDateISO{" DateISO "}%"
print "\\gdef\\VCDateTEX{" DateTEX "}%"
print "\\gdef\\VCTime{" Time "}%"
print "\\gdef\\VCModifiedText{\\textcolor{red}{with local modifications!}}%"
print "%%% Assume clean working copy."
print "\\gdef\\VCModified{0}%"
print "\\gdef\\VCRevisionMod{\\VCRevision}%"
}
### Process output of "git status".
if (script=="status") {
print "%%% Is working copy modified?"
print "\\gdef\\VCModified{" modified "}%"
if (modified==0) {
print "\\gdef\\VCRevisionMod{\\VCRevision}%"
} else {
print "\\gdef\\VCRevisionMod{\\VCRevision~\\VCModifiedText}%"
}
}
}

View File

@@ -0,0 +1,83 @@
% talk-notes.sty -- capture speaker notes for assembly outside LaTeX.
%
% The ltx-talk class has no \note command (upstream issue #156 is open, and the
% maintainer has said notes are wanted but unimplemented). This package fills
% the gap without touching the slides themselves: \note{...} typesets nothing
% and instead records its text to \jobname.notes, which the mknotes program
% reads to build the presenter PDF.
%
% The awkward part of doing this outside LaTeX is that a frame does not
% correspond to a page: overlays expand one frame into several slides, and the
% source cannot predict how many. That is why the page records are written from
% the shipout hook, which is the first point at which the frame and slide
% identity of a page is actually known.
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{talk-notes}[2026-08-14 v0.1 Capture speaker notes for ltx-talk]
\ExplSyntaxOn
\iow_new:N \g__talknotes_iow
\tl_new:N \g__talknotes_section_tl
\tl_new:N \g__talknotes_subsection_tl
\AddToHook { begindocument }
{ \iow_open:Nn \g__talknotes_iow { \c_sys_jobname_str .notes } }
% The stream is deliberately not closed by hand: LaTeX closes it at the end of
% the run, and closing it from the enddocument hook races the final shipout.
% Track the section and subsection so the notes page can show where we are.
% ltx-talk exposes the current title as \l__talk_section_tl; guard the lookup
% so that a class change does not turn this into an error.
\AddToHook { section/begin }
{
\cs_if_exist:NT \l__talk_section_tl
{
\tl_gset:NV \g__talknotes_section_tl \l__talk_section_tl
\tl_gclear:N \g__talknotes_subsection_tl
}
}
\AddToHook { subsection/begin }
{
\cs_if_exist:NT \l__talk_subsection_tl
{ \tl_gset:NV \g__talknotes_subsection_tl \l__talk_subsection_tl }
}
% One record per shipped page.
\AddToHook { shipout/before }
{
\iow_now:Nx \g__talknotes_iow
{
PAGE ~ \arabic { page } |
FRAME ~ \int_use:N \g__talk_frame_int |
SLIDE ~ \int_use:N \g__talk_slide_int
}
\iow_now:Nx \g__talknotes_iow
{ SECTION ~ \tl_to_str:N \g__talknotes_section_tl }
\iow_now:Nx \g__talknotes_iow
{ SUBSECTION ~ \tl_to_str:N \g__talknotes_subsection_tl }
}
% \note<overlay spec>[item]{text}
%
% Follows beamer's interface. A plain \note contributes running text; \note[item]
% contributes an entry to a numbered list printed after that text.
%
% The body is written out stringified, so LaTeX markup inside a note survives
% to be typeset again on the notes page. A note is recorded once per slide of
% its frame (the frame body is re-run for each), so mknotes deduplicates.
% Records are bracketed by ENDNOTE because a long note may be broken across
% output lines.
\NewDocumentCommand \note { d<> o +m }
{
\iow_now:Nx \g__talknotes_iow
{
NOTE ~ FRAME ~ \int_use:N \g__talk_frame_int |
SPEC ~ \IfNoValueTF {#1} { - } { \tl_to_str:n {#1} } |
KIND ~ \IfNoValueTF {#2} { text } { \tl_to_str:n {#2} }
}
\iow_now:Nx \g__talknotes_iow { TEXT ~ \tl_to_str:n {#3} }
\iow_now:Nn \g__talknotes_iow { ENDNOTE }
}
\ExplSyntaxOff

316
slides_template/trantor.sty Normal file
View File

@@ -0,0 +1,316 @@
% trantor.sty -- a Metropolis-style theme for the ltx-talk class.
% LuaLaTeX only.
%
% Copyright (c) 2026 Marco Pompili, https://github.com/mrc-pop/trantor
% Received under the MIT license and redistributed here, with modifications,
% under the GNU GPL version 3 or later. See COPYING.
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{trantor}[2026-08-01 v1.0 Metropolis-like theme for ltx-talk]
% LuaTeX only (XeTeX is unsupported and the fonts need a Unicode engine).
\RequirePackage{iftex}
\RequireLuaTeX
\RequirePackage{tikz}
\RequirePackage{pifont}
% -- Fonts. Upstream sets Libertinus and loads lum-euler; both are left to the
% document, which must \setsansfont, \setmonofont and load a maths font.
\renewcommand{\familydefault}{\sfdefault}
\linespread{1.18}
\providecommand\headerfont{\sffamily}
\makeatletter
% Tighten the space around display maths -- the default 11pt reads as a big gap
% on a slide, especially inside a list. Re-applied on every \normalsize so the
% size files don't reset it.
\g@addto@macro\normalsize{%
\setlength\abovedisplayskip{5pt plus 2pt minus 1pt}%
\setlength\belowdisplayskip{5pt plus 2pt minus 1pt}%
\setlength\abovedisplayshortskip{5pt plus 2pt minus 1pt}%
\setlength\belowdisplayshortskip{5pt plus 2pt minus 1pt}%
}
% -- Colours. Set your own accent anywhere in the preamble with, e.g.,
% \definecolor{accent}{HTML}{BF1238}
% \definecolor{TrantorDark}{HTML}{23373B}
% Four colour palette:
% 1. accent progress bars, title rule
% 2. TrantorDark headings, section dividers, standout panels, frame numbers
% 3. TrantorLight text set against TrantorDark
% 4. TrantorMuted affiliation, footline, arxiv. Lightest grey that still meets
% WCAG AA contrast (4.5:1) on white (~4.54:1).
\providecolor{accent}{HTML}{2A7AE2}
\providecolor{TrantorDark}{HTML}{000000}
\providecolor{TrantorLight}{HTML}{FFFFFF}
\providecolor{TrantorMuted}{HTML}{767676}
\AddToHook{begindocument}{\colorlet{TrantorAccent}{accent}}
% itemize/enumerate labels in the body colour rather than the class default
\AddToHook{begindocument}{\colorlet{structure}{TrantorDark}}
% -- Header bar carrying the frame title (hidden on plain pages).
% headsep is what pads under the title; the height key alone only pads above.
\geometry{headsep=10pt}
\AddToHook{begindocument}{%
\EditInstance{header}{std}{
background-color = TrantorDark,
color = TrantorLight,
print-frame-title = true,
height = 40pt,
font = \headerfont\bfseries,
left-hspace = \dimexpr\Gm@lmargin - 0.6cm\relax,
}%
\EditInstance{footer}{std}{element-order = {}}%
\EditInstance{frametitle}{header}{after-vspace = 6mm minus 3mm}%
}
% Like \frametitle, but forces a gap below the title on packed slides where the
% header's own spacing collapses. The \par gets us into vertical mode first.
\NewDocumentCommand\stitle{m}{\frametitle{#1}\par\addvspace{4mm}}
% \standoutline{...}: a centred line of text in the title's sans/size/colour,
% for emphasis within a normal (light) frame, e.g. closing a slide.
\NewDocumentCommand\standoutline{m}{%
\par
\begingroup
\centering\headerfont\Large\bfseries\color{TrantorDark}#1\par
\endgroup
}
% Tick and cross, for marking things right or wrong.
\NewDocumentCommand\yes{}{\ding{51}}
\NewDocumentCommand\no{}{\ding{55}}
% \footline{...}: a small reference or citation pinned to the bottom-left of
% the slide. Overlaid, so it can go anywhere inside the frame.
\NewDocumentCommand\footline{m}{%
\begin{tikzpicture}[remember picture, overlay]
\node[anchor=south west, xshift=1ex, yshift=0.7ex, font=\scriptsize, text=TrantorMuted]
at (current page.south west) {#1};
\end{tikzpicture}%
}
% \arxiv{1306.5001}: a small, grey "arXiv:<id>" tag linked to the abstract.
% Meant to sit inline next to a statement, e.g. a theorem.
\NewDocumentCommand\arxiv{m}{%
\href{https://arxiv.org/abs/#1}{{\headerfont\scriptsize\color{TrantorMuted}arXiv:#1}}%
}
% -- \appendix: frames after it are numbered among themselves and left out of
% the total. The count travels through the aux file, so it settles on the
% second run.
\newif\iftrantorAppendix
\trantorAppendixfalse
\newcommand\trantorMainFrames{\@totalframes}
\NewDocumentCommand \appendix { } {%
\global\trantorAppendixtrue
\protected@write\@auxout{}{%
\string\gdef\string\trantorMainFrames{\the\value{frame}}}%
}
% Count registers, not macros: pgfmath expands while parsing, and a macro
% carrying a conditional loops.
\newcount\trantor@n
\newcount\trantor@d
\newcommand\trantor@setcounts{%
\iftrantorAppendix
\trantor@n=\numexpr\value{frame}-\trantorMainFrames\relax
\trantor@d=\numexpr\@totalframes-\trantorMainFrames\relax
\else
\trantor@n=\value{frame}\relax
\trantor@d=\numexpr\trantorMainFrames\relax
\fi
}
% Fraction of the way through the talk, shared by the footer bar and
% \trantorProgressBar. The zero test must stay outside pgfmath, which
% evaluates both arms of a ternary.
\newcommand\trantor@setratio{%
\trantor@setcounts
\ifnum\trantor@d>0
\pgfmathsetmacro{\trantorratio}{min(\the\trantor@n / \the\trantor@d, 1)}%
\else
\def\trantorratio{0}%
\fi
}
% \trantorProgressBar{<width>}: a rule of the given width, filled to the
% current position and grey the rest of the way.
\newlength\trantor@barwidth
\newlength\trantor@barfill
\NewDocumentCommand \trantorProgressBar { m } {%
\setlength\trantor@barwidth{#1}%
\trantor@setratio
% dimen = <factor><dimen>, so \trantorratio scales the width
\setlength\trantor@barfill{\trantorratio\trantor@barwidth}%
\textcolor{TrantorAccent}{\rule{\trantor@barfill}{0.6pt}}%
\textcolor{TrantorDark!20}{%
\rule{\dimexpr\trantor@barwidth-\trantor@barfill\relax}{0.6pt}}%
}
% -- Bottom progress bar + frame counter (hidden on plain pages).
\newif\iftrantorPlainPage
\trantorPlainPagefalse
\AddToHook{shipout/foreground}{%
\iftrantorPlainPage\else
\trantorArtifactBegin
\begin{tikzpicture}[remember picture, overlay]
\trantor@setratio
\fill[TrantorAccent]
(current page.south west)
rectangle
([xshift=\trantorratio\paperwidth, yshift=1pt] current page.south west);
\node[anchor=south east, inner sep=5pt, font=\tiny, text=TrantorDark]
at (current page.south east) {\the\trantor@n\,/\,\the\trantor@d};
\end{tikzpicture}%
\trantorArtifactEnd
\fi
\global\trantorPlainPagefalse
}
% -- Remember the section and subsection titles, for the dividers.
\ExplSyntaxOn
\tl_new:N \g__trantor_section_tl
\AddToHook{section/begin}{
\cs_if_exist:NT \l__talk_section_tl {
\tl_gset:NV \g__trantor_section_tl \l__talk_section_tl
}
}
\cs_new_protected:Npn \__trantor_print_section: { \tl_use:N \g__trantor_section_tl }
\cs_set_eq:NN \trantorSectionTitle \__trantor_print_section:
% Likewise the subsection, for \subsectionpage. Cleared when a new section
% starts so a stale title cannot leak into the next one.
\tl_new:N \g__trantor_subsection_tl
\AddToHook{section/begin}{ \tl_gclear:N \g__trantor_subsection_tl }
\AddToHook{subsection/begin}{
\cs_if_exist:NT \l__talk_subsection_tl {
\tl_gset:NV \g__trantor_subsection_tl \l__talk_subsection_tl
}
}
\cs_new_protected:Npn \__trantor_print_subsection: { \tl_use:N \g__trantor_subsection_tl }
\cs_set_eq:NN \trantorSubsectionTitle \__trantor_print_subsection:
% Suspend tagging around the theme's decorative / pagination graphics when
% tagging is on (a no-op otherwise), so the progress bar and frame counter stay
% out of the structure tree instead of being flagged as loose content.
\cs_new_protected:Npn \trantorArtifactBegin
{ \cs_if_exist:NT \tag_suspend:n { \tag_suspend:n { trantor } } }
\cs_new_protected:Npn \trantorArtifactEnd
{ \cs_if_exist:NT \tag_resume:n { \tag_resume:n { trantor } } }
\ExplSyntaxOff
% -- Title page. Drop a logo.png next to your slides, or redefine \titlelogo.
\newcommand{\titlelogo}{%
\IfFileExists{logo.png}
{\includegraphics[height=1.4cm, alt={Organisation logo}]{logo.png}}{}%
}
\renewcommand{\maketitle}{%
\global\trantorPlainPagetrue
\begin{frame}{}%
\thispagestyle{plain}%
\vfill
\hspace*{0.04\paperwidth}%
\begin{minipage}{0.82\paperwidth}
\raggedright
{\color{TrantorDark}\headerfont\Large\bfseries \@title\par}%
\ifx\@subtitle\empty\else
\vspace{0.35em}%
{\color{TrantorDark}\headerfont\large \@subtitle\par}%
\fi
\vspace{0.5em}%
{\color{TrantorAccent}\rule{\linewidth}{0.8pt}\par}%
\vspace{1.3em}%
% Metadata on the left, logo on the right; bottom-aligned so the logo sits
% level with the date and grows upward rather than below it.
\begin{minipage}[b]{0.66\linewidth}
\raggedright
{\normalsize \@author\par}%
\ifx\@institute\empty\else
\vspace{0.2em}%
{\small\color{TrantorMuted} \@institute\par}%
\fi
\vspace{0.2em}%
{\small \@date\par}%
\end{minipage}%
\hfill
\begin{minipage}[b]{0.30\linewidth}
\raggedleft
\titlelogo
\end{minipage}%
\end{minipage}%
\hspace*{0.04\paperwidth}%
\vspace{0.12\paperheight}%
\end{frame}%
}
% -- A full-bleed dark panel with the given body centred in a minipage of the
% given width.
\NewDocumentCommand \trantor@panel { m +m } {%
\global\trantorPlainPagetrue
\begin{frame}{}%
\thispagestyle{plain}%
\noindent
\begin{tikzpicture}
\useasboundingbox (0,0) rectangle (\linewidth,-\textheight);
\fill[TrantorDark]
(-\Gm@lmargin, \paperheight)
rectangle
(\dimexpr\paperwidth-\Gm@lmargin\relax, -\paperheight);
\node[text=TrantorLight, anchor=center, inner sep=0pt]
at (0.5\linewidth, -0.5\textheight) {%
\begin{minipage}{#1}
\centering
#2
\end{minipage}%
};
\end{tikzpicture}%
\end{frame}%
}
% -- Divider pages: the section title over a progress bar, set on the ordinary
% light background. Not plain pages: the footer bar and frame number stay.
\NewDocumentCommand \trantor@divider { +m } {%
\begin{frame}{}%
\thispagestyle{plain}%
\vfill
\centering
\begin{minipage}{0.52\linewidth}%
\raggedright
{\headerfont\Large\bfseries\trantorSectionTitle\par}%
\vspace{0.4em}%
\trantorProgressBar{\linewidth}%
#1%
\end{minipage}%
\par
\vfill
\end{frame}%
}
\NewDocumentCommand \sectionpage { } { \trantor@divider{} }
% The subsection divider is the same, naming the subsection under the bar.
\NewDocumentCommand \subsectionpage { } {%
\trantor@divider{%
\par\vspace{0.8em}%
{\headerfont\large\bfseries\trantorSubsectionTitle\par}%
}%
}
% \standout{...}: a full-bleed dark panel with the given text centred on it.
\NewDocumentCommand \standout { +m } {%
\trantor@panel{0.8\linewidth}{\headerfont\LARGE\bfseries #1}%
}
\makeatother