20
0

Share the font setup between the slides and the notes

The notes pages were typeset in the default Latin Modern while the slides
used OpenSans and RobotoMono, so the two halves of a presenter page did not
match.

Move the font declarations to fonts.tex, which the slides \input and
mknotes passes to the notes document. mknotes uses it by default when it
exists, so a deck that copies the two notes files in gets matching fonts
without having to remember a flag; --preamble names a different fragment,
and a file named there but missing is an error rather than ignored.

The notes document now compiles from the document directory with
-output-directory rather than running inside the temporary one, so the
relative font paths in the fragment resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-14 07:21:24 -07:00
parent 01534406dc
commit 27304c853a
2 changed files with 53 additions and 5 deletions

View File

@@ -151,6 +151,10 @@ def attach_notes(pages, notes):
#: 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.
@@ -159,11 +163,13 @@ NOTES_PREAMBLE = r"""\documentclass{article}
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}
"""
@@ -179,7 +185,7 @@ def escape_for_context(text):
def build_notes_pdf(pages, width, height, thumb_w, thumb_h, workdir,
jobname="notes"):
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,
@@ -198,7 +204,14 @@ def build_notes_pdf(pages, width, height, thumb_w, thumb_h, workdir,
textwidth = width - hmargin * 2
bandwidth = width - thumb_w
body = [NOTES_PREAMBLE % {"width": width, "height": height}]
# \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):
@@ -249,9 +262,12 @@ def build_notes_pdf(pages, width, height, thumb_w, thumb_h, workdir,
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", jobname + ".tex"],
cwd=workdir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
["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):
@@ -294,6 +310,10 @@ def main():
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 "
@@ -321,10 +341,19 @@ def main():
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))
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"