Pulls poster submissions from a Google Sheet (Google Form responses) and their associated abstract-graphic images from Google Drive, then builds:
- a PDF poster book (
build/latex/main.pdf): title page, table of contents, and one uniformly-formatted page per submission. - a companion slide deck (
build/latex/slides.pdf): a splash slide followed by one slide per submission showing just the title, presenter, image, and session/poster number.
Both are generated from the same fetched data and the same
event/config.yaml.
engine/ reusable, event-agnostic: fetch/render scripts + LaTeX core
event/ this event's config, credentials, and front matter
build/ everything generated (gitignored, safe to delete anytime)
engine/is the part meant to be copied wholesale for a future event. See "Reusing this for a future event" below.event/is Mercury 2026-specific:config.yaml,service_account.json(both gitignored),main.tex(the booklet),slides.tex(the slide deck), andassets/(e.g. a logo image, if you have one).build/is 100% generated —build/data/(fetched JSON + images) andbuild/latex/(rendered content, including the title/splash slide, plus the compiled PDFs and aux files). Nothing inengine/orevent/ever gets written to during a build, and there's no.texfile to hand-edit for any of the content in "Front matter options" or "Slide deck options" below.
- Create/select a project at https://console.cloud.google.com.
- Enable the Google Sheets API and Google Drive API for it.
- Create a Service Account (IAM & Admin > Service Accounts), then
create a JSON key for it and download it as
event/service_account.json(it's gitignored). - Share the response Google Sheet with the service account's email
(found in the JSON key as
client_email), Viewer access is enough. - Share the Drive files/folder holding the abstract graphics with that same service account email, Viewer access. The script downloads each image directly by the file id embedded in the sheet's "Abstract Graphic" URL, so the folder itself doesn't need to be listed — just readable.
pip install -r engine/requirements.txt
cp event/config.example.yaml event/config.yaml
# edit event/config.yaml: spreadsheet_id, sheet_name, front_matter sectionYou'll also need a LaTeX distribution with latexmk (e.g. TeX Live,
MacTeX) to build the PDF.
Everything on the title page is set in event/config.yaml's
front_matter section — no .tex editing needed:
| Field | Effect if blank/omitted |
|---|---|
event_title, event_subtitle |
Falls back to generic text |
venue |
The "Held at ..." line is omitted entirely |
event_date |
Uses today's date (\today) at build time |
logo_file |
Path to a logo image, relative to the repo root (put the file in event/assets/, e.g. event/assets/logo.png). Shows a "LOGO HERE" placeholder box if blank or the file doesn't exist |
poster_sessions |
A list of {location, time} entries, one per session (add/remove entries for a different number of sessions); a blank time is just omitted from that session's line. Also used for the room/time shown on each session's divider page in the book — see "Poster session assignment" below |
Each presenter (by email) is assigned a session group and a poster number
within that group — set in event/config.yaml:
max_people_per_session: presenters are split into groups of at most this many people.assignments_file: where the group/poster-number assignments are persisted (defaultevent/poster_assignments.yaml), keyed by email. Created automatically on first run.
Every fetch is deterministic and stable: existing entries in
assignments_file are never reassigned, so re-running after new
submissions come in only adds new emails — it fills the earliest group
with room (poster numbers restart at 1 for each new group), and never
reshuffles anyone already assigned. This file contains attendee email
addresses, so it's gitignored — but unlike build/, it's not
disposable; deleting it resets everyone's assignment.
The book is ordered by (group, poster number), with a full-page divider
between groups showing that session's room/time from
front_matter.poster_sessions (group 1 uses the first entry, group 2 the
second, and so on). If more groups end up in use than you've configured
poster_sessions entries for, generate_latex.py prints a warning and
those extra dividers just show the group number with no room/time.
A row with no email address can't be matched reliably, so it's left unassigned (a warning is printed) and placed at the very end of the book under an "Unassigned" divider rather than blocking the build.
The splash slide is set in event/config.yaml's slides section:
| Field | Effect if blank/omitted |
|---|---|
event_title, event_subtitle |
Falls back to front_matter's title/subtitle, then generic text |
event_date |
Omitted entirely (unlike the booklet, there's no \today fallback — it's meant to be optional) |
background_image |
Path to an image, relative to the repo root, shown behind every slide (splash + posters). Blank or missing -> plain white background |
session_label_position |
"left", "center", or "right" — where the "Session N, Poster M" label sits at the bottom of each poster slide, so you can dodge a busy spot in background_image. Anything else falls back to "left" with a warning |
title_width |
A fraction of the slide width, (0, 1], that the title/presenter block is allowed to span on each poster slide — narrow it to nudge long titles inward, away from a busy background_image. Missing or invalid falls back to 0.9 with a warning |
The splash slide's logo reuses front_matter.logo_file (same placeholder
box if blank/missing). Every poster gets one slide, in the same
(group, poster number) order as the booklet, showing only the title,
presenter, image, and a "Session N, Poster M" label — none of the
abstract text, full author list, degree/advisor fields, or session
room/time appear on slides.
Python (engine/requirements.txt):
google-api-python-client— Sheets/Drive API callsgoogle-auth— service-account authenticationgoogle-auth-httplib2— HTTP transport for the auth credentialsPyYAML— readsconfig.yamlJinja2— renders the LaTeX templatepylatexenc— converts Unicode in abstract text (Greek letters, smart quotes, accented names, etc.) into valid LaTeX automatically
LaTeX (from your TeX distribution; all are standard and included in a full TeX Live/MacTeX install):
latexmk— build driver (re-runs pdflatex as needed for the TOC)geometry— page marginshyperref— PDF bookmarks/TOC linksgraphicx— image inclusionpgfkeys— key-value options for the\posterpageenvironmentparskip— paragraph spacingbeamer— the slide deck (event/slides.tex)
./build.shThis runs, in order:
engine/scripts/fetch_sheet_data.py --config event/config.yaml— reads the sheet; if the same email submitted more than once, keeps only the most recent (by the sheet'sTimestampcolumn) and prints a warning for each one dropped; sorts the remaining rows alphabetically by presenting-author last name; assigns each new presenter a session group + poster number (see "Poster session assignment" below); downloads each row's Abstract Graphic image; and writesbuild/data/posters.json+build/data/images/.engine/scripts/generate_latex.py— renders every generated.texfile underbuild/latex/content/(the booklet's page content and title page, the slide deck's title/poster slides and background/layout setup) from that JSON andevent/config.yaml, via the Jinja2 templates inengine/latex/templates/.- Two
latexmk -pdf -output-directory=../build/latexruns fromevent/— one formain.tex(the booklet, ->build/latex/main.pdf), one forslides.tex(the slide deck, ->build/latex/slides.pdf).
Run ./build.sh --skip-fetch to skip step 1 and reuse the existing
build/data/posters.json + images — handy when you're only iterating on
config.yaml or the LaTeX/templates. Run the Python steps independently
(see their --help) if you want to inspect build/data/posters.json
before building the PDFs.
engine/ doesn't know anything about Mercury 2026 — copy it (and
build.sh, adjusting paths if you rename folders) into a new project,
then:
- Create a fresh
event/with its ownconfig.yaml(copyconfig.example.yaml) andmain.tex/slides.tex(copy the existing ones — both are already generic, just a few\inputlines each). - Edit
COLUMN_MAPat the top ofengine/scripts/fetch_sheet_data.pyto match the new form's actual header row — that's the one place this event's Google Form wording is hardcoded. - Fill in
event/config.yaml: spreadsheet ID, service account key, and thefront_matterandslidessections (title, date, logo, sessions, background image).
Everything else (the \posterpage and \posterslide LaTeX commands, the
front matter/splash slide layouts, the Jinja2 templates, the Unicode
sanitization) is generic and shouldn't need touching.
- Degree program / year / advisor are hidden by default on each poster
page — flip
\showmetadatatrueinengine/latex/preamble.texonce you want that line to appear. The poster number is always shown regardless. - Unicode in abstracts (Greek letters, smart quotes, accented names,
etc.) is converted to LaTeX automatically by
pylatexenc— e.g.πbecomes\ensuremath{\pi}. If a character has no known LaTeX representation, it's replaced with a bold?placeholder in the PDF andgenerate_latex.pyprints a warning naming the character, so it's easy to spot and add a fix for.