More Projects
59 in total
Saturnalia Band QR System
Saturnalia 2025 - Thapar Institute · 2025
Generator for the wristband QR codes used at Saturnalia 2025. A Pillow + qrcode script mints 600 random alphanumeric passes — 120 SAT25I 'infinite' multi-day bands and 480 SAT25S single-use ones — each rendered at high error correction with the festival logo composited into the centre and, for the exclusive tier, an 'EXCLUSIVE' label fitted into the quiet zone on all four sides with rotated text for the vertical margins. Every code is written to a JSON registry carrying its tier, a redemption counter and per-day (14/15/16 Nov) scan flags for gate staff.
Project Details
Saturnalia 2025 - Thapar Institute
2025
Tool · Backend
Saturnalia 2025 sold two kinds of wristband: multi-day passes and single-entry ones. Gate staff had to tell them apart on sight and know whether a given band had already been used that day. That is a printing problem and a state problem at once, both settled before the bands went to print.
The reason it had to be settled beforehand is that a wristband is not a database row you can alter. Once six hundred of them are printed, laminated and boxed, every property of the code is frozen: the tier, the visual treatment, the amount of the code the logo is allowed to cover, and whether the printed size leaves enough quiet zone for a phone camera at arm's length in the dark. The script exists because all of those had to be right on the first pass.
How the codes are built
Each pass is a six-character random string over uppercase letters and digits, prefixed SAT25I for
the infinite tier or SAT25S for single use. The QR renders at error correction level H, the only
level that survives what happens next: the festival logo is composited into the centre at 28% of the
code's width for infinite bands and 32% for single-use ones, covering enough modules that the reader
has to reconstruct them.
The exclusive tier gets a second treatment. Rather than growing the image, the word EXCLUSIVE is fitted into the quiet zone that already surrounds the code. The script measures each side's real margin by diffing the render against a white background and taking the bounding box of non-white content, then shrinks the font from 80px in steps of 4 until the text fits with 4px of padding. Top and bottom draw directly; left and right render to a transparent scratch image, rotate 90 degrees, and paste back. The border stays at 2 modules rather than the spec default of 4, keeping the printed band tight without losing the scannable margin.
Two details there are worth pulling out. The margin measurement is deliberately empirical rather than computed: the script could multiply the border setting by the box size and get the same number arithmetically, but diffing the rendered image against white also catches whatever the logo paste and the resize did to the actual pixels. And the shrink loop returns nothing if the text will not fit at eight points, so a side with no room simply goes unlabelled rather than overprinting the code. Failing to draw is the correct failure for something that is about to be printed six hundred times.
The registry
The other half of the output is data.json, keyed by code:
"SAT25I59VXDO": { "type": "infinite", "14nov": false, "15nov": false, "16nov": false, "count": 0 }
Three independent booleans rather than one "used" flag, because the whole point of the infinite tier is that it redeems once per day and not twice. A single flag would have forced the gate app to carry date logic; three flags mean the scan handler picks today's key and sets it. Single-use bands carry the same three flags and no counter, so one schema covers both tiers and the tier field decides how the gate reads it.
What shipped
- 600 codes in one run: 120 infinite, 480 single use.
- A JSON registry keyed by code, carrying its tier and independent scan flags for 14, 15 and 16 November, so a multi-day band redeems once per day.
- A redemption counter on infinite passes, showing total use, not just today's state.
- Per-tier styling with distinct fill colours and separate logo assets, so the two bands differ before anyone scans anything. Infinite is a near-black olive, single-use a deep navy, at box size 12 rather than the default 10 so the printed modules stay chunky.
- Font discovery probing Windows system TTFs, with measurement falling back through
getsizeandtextbboxacross Pillow versions. - 481 and 121 PNGs on disk, the evidence this ran to production rather than staying a script.
What the folders on disk actually say
There were two full runs, a day apart, and they share no codes because the generator draws fresh random strings every time. The first produced the 481 and 121 files above, and its registry lists exactly 600 of them, which means two PNGs in that folder have no entry in the JSON beside them. They are leftovers from an earlier partial run that the rewrite did not clean up. A band printed from one of those two would scan to a code the gate app has never heard of.
The second run is the one that went to print: a folder of exactly 120 and 480, and a registry whose keys match the filenames one for one. Regenerating rather than patching was the right call, because the failure mode of a half-updated registry is worse than the cost of reprinting a proof.
The other honest gap is in the labelling. _find_system_font walks a list of six Windows font paths
and returns the first that exists, and that font is used for the initial text measurement. The loop
that actually fits and draws the label only consults an explicit font_path argument, which main
never passes, so it falls through to Pillow's built-in bitmap font. The label renders and fits
correctly; it just is not rendered in Arial as the code reads like it should be. Nobody at a gate
noticed, which is the only reason it is still like that.
Project Details
Saturnalia 2025 - Thapar Institute
2025
Tool · Backend