← back to CelebritySignatures
Mural QR: B+C — functional-size centre QR + short /k/<code> route
84da440736253d71e2814b034bd72b92384ff620 · 2026-06-18 17:49:42 -0700 · SteveStudio2
DTD verdict (1-1 split, broken on measured fact + argument): render the centre
QR as a functional utility mark, not a uniform signature cell — an 84px cell is
2.3px/module (dead in print) while ~165px lands at 5px/module (print-reliable).
Combined with C: server.js /k/<code> single-letter redirect map → short 47-char
URL → only 33 modules. build-collages.py drops the cell-insertion, draws a crisp
centred QR (NEAREST integer scaling) on a tasteful panel captioned SCAN TO REVEAL
NAMES. All 6 QRs decode-verified to the short URL; /k/d..t serve the named keys
(HTTP 200) locally.
NOTE: QR host is now https://celebrity.designerwallcoverings.com (public) — that
domain is NOT live yet, so scans won't resolve until it's deployed (Steve-gated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M output/collage-declaration-of-independence.pngM output/collage-hollywood.pngM output/collage-movies-classic.pngM output/collage-politics.pngM output/collage-sports.pngM output/collage-tv.pngM scripts/build-collages.pyM server.js
Diff
commit 84da440736253d71e2814b034bd72b92384ff620
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Thu Jun 18 17:49:42 2026 -0700
Mural QR: B+C — functional-size centre QR + short /k/<code> route
DTD verdict (1-1 split, broken on measured fact + argument): render the centre
QR as a functional utility mark, not a uniform signature cell — an 84px cell is
2.3px/module (dead in print) while ~165px lands at 5px/module (print-reliable).
Combined with C: server.js /k/<code> single-letter redirect map → short 47-char
URL → only 33 modules. build-collages.py drops the cell-insertion, draws a crisp
centred QR (NEAREST integer scaling) on a tasteful panel captioned SCAN TO REVEAL
NAMES. All 6 QRs decode-verified to the short URL; /k/d..t serve the named keys
(HTTP 200) locally.
NOTE: QR host is now https://celebrity.designerwallcoverings.com (public) — that
domain is NOT live yet, so scans won't resolve until it's deployed (Steve-gated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
output/collage-declaration-of-independence.png | Bin 648469 -> 615467 bytes
output/collage-hollywood.png | Bin 798780 -> 786375 bytes
output/collage-movies-classic.png | Bin 834063 -> 817862 bytes
output/collage-politics.png | Bin 588463 -> 569529 bytes
output/collage-sports.png | Bin 355659 -> 334063 bytes
output/collage-tv.png | Bin 169526 -> 148098 bytes
scripts/build-collages.py | 58 ++++++++++++++++---------
server.js | 8 ++--
8 files changed, 43 insertions(+), 23 deletions(-)
diff --git a/output/collage-declaration-of-independence.png b/output/collage-declaration-of-independence.png
index 168520e..0cb1865 100644
Binary files a/output/collage-declaration-of-independence.png and b/output/collage-declaration-of-independence.png differ
diff --git a/output/collage-hollywood.png b/output/collage-hollywood.png
index 3def647..715d8b5 100644
Binary files a/output/collage-hollywood.png and b/output/collage-hollywood.png differ
diff --git a/output/collage-movies-classic.png b/output/collage-movies-classic.png
index dd91d64..482b3af 100644
Binary files a/output/collage-movies-classic.png and b/output/collage-movies-classic.png differ
diff --git a/output/collage-politics.png b/output/collage-politics.png
index ccf1f54..2b8fe1d 100644
Binary files a/output/collage-politics.png and b/output/collage-politics.png differ
diff --git a/output/collage-sports.png b/output/collage-sports.png
index d02524b..2a03871 100644
Binary files a/output/collage-sports.png and b/output/collage-sports.png differ
diff --git a/output/collage-tv.png b/output/collage-tv.png
index cf4e5b5..d71ac13 100644
Binary files a/output/collage-tv.png and b/output/collage-tv.png differ
diff --git a/scripts/build-collages.py b/scripts/build-collages.py
index 40648dd..19c8616 100644
--- a/scripts/build-collages.py
+++ b/scripts/build-collages.py
@@ -137,10 +137,18 @@ def fit(im):
return im.resize((max(1, int(im.width * r)), max(1, int(im.height * r))), Image.LANCZOS)
-def make_qr_cell(data):
- """Render the QR as a signature-sized square (BOX_H tall, uniform with the
- signature cells) so it reads as 'just another signature' in the grid — no
- bigger or more prominent than the rest."""
+# Single-letter QR codes (mirror server.js SHORT map) → short URL → low module
+# count → reliably scannable at a modest size, even printed and viewed at distance.
+CODE = {
+ "declaration-of-independence": "d", "politics": "p", "sports": "s",
+ "hollywood": "h", "movies-classic": "c", "tv": "t",
+}
+QR_PX = 180 # functional utility-mark size (~7px/module on the short /k/<code> URL)
+
+
+def make_qr(data, target_px):
+ """Render a crisp QR for `data`, scaled to ~target_px with integer px-per-module
+ (NEAREST) so the modules stay razor-sharp — critical for camera scanning."""
qr = qrcode.QRCode(
error_correction=qrcode.constants.ERROR_CORRECT_M, # ~15% damage tolerance
box_size=10, border=2, # built-in quiet zone
@@ -148,7 +156,10 @@ def make_qr_cell(data):
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
- return img.resize((BOX_H, BOX_H), Image.LANCZOS) # square, signature-sized
+ modules = qr.modules_count + 2 * qr.border
+ box = max(4, round(target_px / modules))
+ side = box * modules
+ return img.resize((side, side), Image.NEAREST)
def render(category, cells, dropped, named, safe):
@@ -190,6 +201,26 @@ def render(category, cells, dropped, named, safe):
tw = d.textlength(label, font=f)
d.text((cx + (CELL_W - tw) / 2, cy + PAD + BOX_H + 4), label, font=f, fill=MUTED)
+ # centred QR (no-names mural only) — a functional utility mark (not a signature),
+ # sized to actually scan in print; encodes the short /k/<code> redirect URL.
+ if not named:
+ grid_h = rows_ct * cell_h
+ qr = make_qr(f"{BASE_URL}/k/{CODE.get(safe, safe)}", QR_PX)
+ cf = font(14, bold=True)
+ cap = "SCAN TO REVEAL NAMES"
+ cap_w = int(d.textlength(cap, font=cf))
+ pad_in, gap, cap_h = 16, 10, 18
+ panel_w = max(qr.width, cap_w) + pad_in * 2
+ panel_h = qr.height + gap + cap_h + pad_in * 2
+ px = (W - panel_w) // 2
+ py = HEADER_H + (grid_h - panel_h) // 2
+ d.rectangle([px + 5, py + 6, px + panel_w + 5, py + panel_h + 6], fill=(214, 210, 202)) # soft shadow
+ d.rectangle([px, py, px + panel_w, py + panel_h], fill=(255, 255, 255), outline=(150, 146, 138), width=2)
+ qx = px + (panel_w - qr.width) // 2
+ qy = py + pad_in
+ canvas.paste(qr, (qx, qy))
+ d.text((px + (panel_w - cap_w) / 2, qy + qr.height + gap), cap, font=cf, fill=INK)
+
# footer
d.text((PAD + 8, H - FOOTER_H + 14),
"Uniform sizing — no signature emphasized. Deceased or public-domain only; living-celebrity rows excluded (right of publicity).",
@@ -222,22 +253,9 @@ def build(category, rows):
print(f"[{category}] no images resolved, skipping"); return []
safe = re.sub(r"[^a-z0-9]+", "-", category.lower()).strip("-")
-
- # The mural gets the QR dropped in as just another (nameless) signature cell,
- # centred in the grid — same size, same placement as every signature. It links
- # to the named "key" version. render() uses the same cols formula, so inserting
- # at this index lands the QR in the middle row/column of the laid-out grid.
- qr_url = f"{BASE_URL}/k/{safe}" # short → low-density → scans at sig size
- qr_cell = ("", make_qr_cell(qr_url))
- total = len(cells) + 1
- cols = min(10, max(1, round(total ** 0.5)))
- rows_ct = (total + cols - 1) // cols
- center = min((rows_ct // 2) * cols + cols // 2, total - 1)
- mural_cells = cells[:center] + [qr_cell] + cells[center:]
-
made = [
- render(category, cells, dropped, named=True, safe=safe), # the key (QR target)
- render(category, mural_cells, dropped, named=False, safe=safe), # the mural (QR = a cell)
+ render(category, cells, dropped, named=True, safe=safe), # the key (QR target)
+ render(category, cells, dropped, named=False, safe=safe), # the mural (centre QR)
]
return made
diff --git a/server.js b/server.js
index 610ebf6..0812131 100644
--- a/server.js
+++ b/server.js
@@ -19,10 +19,12 @@ createServer(async (req, res) => {
let path = decodeURIComponent(req.url.split('?')[0]);
if (path === '/') path = '/public/index.html';
if (path === '/api/signatures') path = '/data/celebrity_signatures.json';
- // Short QR target: /k/<cat> -> the named "key" mural. A short encoded URL keeps
- // the signature-sized QR low-density so it scans cleanly off a printed wall.
+ // Short QR target: /k/<code> -> the named "key" mural. Single-letter codes keep
+ // the encoded URL tiny so the centre QR stays low-density and scans cleanly off a
+ // printed wall. Full-slug (/k/<cat>) still works as a fallback.
+ const SHORT = { d: 'declaration-of-independence', p: 'politics', s: 'sports', h: 'hollywood', c: 'movies-classic', t: 'tv' };
const k = path.match(/^\/k\/([a-z0-9-]+)$/);
- if (k) path = `/output/collage-${k[1]}-named.png`;
+ if (k) path = `/output/collage-${SHORT[k[1]] || k[1]}-named.png`;
const file = join(ROOT, path);
if (!file.startsWith(ROOT)) { res.writeHead(403).end('forbidden'); return; }
const body = await readFile(file);
← ac66a5d QR points at public Kamatera host via short /k/<cat> route;
·
back to CelebritySignatures
·
Murals storefront: /murals sales page + wall-placement studi 9111112 →