The fill-in fields put the label on its own line with dotted lines to write on. handout.tex takes \studentnum and \studenthex; build-class.py compiles one copy per student with the prefix filled in, checks each is two pages and joins them for double-sided printing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
64 lines
2.7 KiB
Python
Executable File
64 lines
2.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""One exercise sheet per student, with their prefix filled in, in one PDF.
|
|
|
|
python3 build-class.py 100 # students 1..100 -> class.pdf
|
|
python3 build-class.py 100 -o out.pdf
|
|
python3 build-class.py 5 12 -o few.pdf # students 5..12
|
|
|
|
Student n gets 2a0d:d741:<n in hex>::/48; the lab /40 allows 1..255. Each
|
|
sheet is compiled separately and must come out to exactly two pages, so
|
|
the result prints double-sided with one student per leaf. Needs pdflatex
|
|
and pdfunite (poppler).
|
|
"""
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def page_count(pdf):
|
|
out = subprocess.run(["pdfinfo", pdf], capture_output=True, text=True, check=True).stdout
|
|
return int(next(line.split()[-1] for line in out.splitlines() if line.startswith("Pages:")))
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
ap.add_argument("first", type=int, help="first student number, or the count if it is the only number")
|
|
ap.add_argument("last", type=int, nargs="?")
|
|
ap.add_argument("-o", "--output", default=os.path.join(HERE, "class.pdf"))
|
|
args = ap.parse_args()
|
|
first, last = (1, args.first) if args.last is None else (args.first, args.last)
|
|
if not 1 <= first <= last <= 255:
|
|
sys.exit("student numbers must be within 1..255: the lab /40 has 255 usable /48s")
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
for name in os.listdir(HERE):
|
|
if name.endswith((".tex", ".cls", ".sty", ".bst", ".bib")):
|
|
shutil.copy(os.path.join(HERE, name), tmp)
|
|
parts = []
|
|
for n in range(first, last + 1):
|
|
job = f"student-{n:03d}"
|
|
tex = f"\\def\\studentnum{{{n}}}\\def\\studenthex{{{n:x}}}\\input{{handout.tex}}"
|
|
for _ in range(2): # twice, for the figure and table references
|
|
r = subprocess.run(["pdflatex", "-interaction=nonstopmode", "-halt-on-error", "-jobname", job, tex],
|
|
cwd=tmp, capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
sys.exit(f"student {n}: pdflatex failed:\n{r.stdout[-2000:]}")
|
|
pdf = os.path.join(tmp, job + ".pdf")
|
|
pages = page_count(pdf)
|
|
if pages != 2:
|
|
sys.exit(f"student {n}: sheet is {pages} pages, not 2; double-sided printing would misalign")
|
|
parts.append(pdf)
|
|
print(f"\rstudent {n}: 2a0d:d741:{n:x}::/48", end="", flush=True)
|
|
print()
|
|
subprocess.run(["pdfunite", *parts, args.output], check=True)
|
|
print(f"wrote {args.output}: students {first}..{last}, {page_count(args.output)} pages")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|