"""Certificate renderer. Draws the participation certificate directly with Pillow, so the Space has no runtime dependency on any other Space. Fonts come from `fonts-dejavu-core` and `fonts-noto-cjk` (see packages.txt); set CERT_FONT_DIR to render with a different font set. """ import functools import os from PIL import Image, ImageDraw, ImageFont # --- canvas --------------------------------------------------------------- W, H = 2000, 1414 SS = 2 # supersample factor; the final PNG is downsampled to W x H PAPER = (253, 252, 249) GRID = (243, 243, 240) # PAPER under 4.5% ink, i.e. the CSS grid lines INK = (31, 41, 55) ORANGE = (234, 88, 12) MUTED = (107, 114, 128) SEP = (211, 214, 220) FONT_DIRS = [ os.environ.get("CERT_FONT_DIR"), "/usr/share/fonts/truetype/dejavu", "/usr/share/fonts/dejavu", "/usr/share/fonts/TTF", "/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype/noto", ] SERIF_BOLD, SANS, SANS_BOLD, MONO_BOLD = ( "DejaVuSerif-Bold.ttf", "DejaVuSans.ttf", "DejaVuSans-Bold.ttf", "DejaVuSansMono-Bold.ttf", ) # Participant names are arbitrary text, and DejaVu has no CJK coverage, so fall # back to Noto for anything it can't draw (otherwise those names come out as # tofu boxes). Debian splits the CJK weights across fonts-noto-cjk and # fonts-noto-cjk-extra, so try both weights rather than assuming Bold exists. BOLD_FALLBACKS = ("NotoSansCJK-Bold.ttc", "NotoSansCJK-Regular.ttc") FALLBACKS = { SERIF_BOLD: BOLD_FALLBACKS, SANS_BOLD: BOLD_FALLBACKS, MONO_BOLD: BOLD_FALLBACKS, SANS: ("NotoSansCJK-Regular.ttc", "NotoSansCJK-Bold.ttc"), } def font_path(filename, required=True): for d in FONT_DIRS: if d and os.path.exists(os.path.join(d, filename)): return os.path.join(d, filename) if not required: return None raise FileNotFoundError( f"{filename} not found in {[d for d in FONT_DIRS if d]}. " "Install fonts-dejavu-core or set CERT_FONT_DIR." ) @functools.lru_cache(maxsize=None) def font(filename, size): return ImageFont.truetype(font_path(filename), int(size * SS)) @functools.lru_cache(maxsize=None) def fallback_path(filename): """First installed fallback font for `filename`, or None if none is.""" for candidate in FALLBACKS.get(filename, ()): path = font_path(candidate, required=False) if path: return path return None @functools.lru_cache(maxsize=None) def fallback_font(filename, size): path = fallback_path(filename) return ImageFont.truetype(path, int(size * SS)) if path else None @functools.lru_cache(maxsize=None) def covered(path): """Codepoints the font can actually draw (empty set = assume everything).""" if path is None: return frozenset() try: from fontTools.ttLib import TTCollection, TTFont f = TTCollection(path).fonts[0] if path.endswith(".ttc") else TTFont(path, fontNumber=0) with f: return frozenset(f.getBestCmap()) except Exception: return frozenset() # coverage unknown -> draw with the primary font def runs(text, filename, size): """Split `text` into (string, font) runs, using the fallback font for characters the primary font lacks and dropping ones neither can draw.""" primary, fb = font(filename, size), fallback_font(filename, size) main_cmap, fb_cmap = covered(font_path(filename)), covered(fallback_path(filename)) out = [] for ch in text: if not main_cmap or ord(ch) in main_cmap or ch == " ": f = primary elif fb is not None and (not fb_cmap or ord(ch) in fb_cmap): f = fb else: continue # no font can draw it; a tofu box would look worse if out and out[-1][1] is f: out[-1][0] += ch else: out.append([ch, f]) return [(t, f) for t, f in out] def runs_width(rs): return sum(f.getlength(t) for t, f in rs) def fit_runs(text, filename, size, max_width, min_size): """Shrink to fit `max_width`, then ellipsize if it still doesn't fit.""" while size > min_size: rs = runs(text, filename, size) if runs_width(rs) <= max_width * SS: return rs size -= 2 while text: rs = runs(text + "…", filename, min_size) if runs_width(rs) <= max_width * SS: return rs text = text[:-1] return runs("…", filename, min_size) def fit_font(filename, size, text, max_width, min_size=32): """Largest size <= `size` at which `text` fits in `max_width` design px.""" while size > min_size: f = font(filename, size) if f.getlength(text) <= max_width * SS: return f size -= 2 return font(filename, min_size) def line_height(f): return sum(f.getmetrics()) # --- text helpers --------------------------------------------------------- def tracked_width(f, text, tracking): return sum(f.getlength(c) for c in text) + tracking * SS * max(len(text) - 1, 0) def draw_tracked(draw, cx, y, text, f, fill, tracking): """Centered text with CSS-style letter-spacing (Pillow has no tracking).""" x = cx - tracked_width(f, text, tracking) / 2 for ch in text: draw.text((x, y), ch, font=f, fill=fill, anchor="la") x += f.getlength(ch) + tracking * SS return y + line_height(f) def draw_centered(draw, cx, y, text, f, fill): draw.text((cx, y), text, font=f, fill=fill, anchor="ma") return y + line_height(f) def draw_runs_centered(draw, cx, y, rs, fill): x = cx - runs_width(rs) / 2 for text, f in rs: draw.text((x, y), text, font=f, fill=fill, anchor="la") x += f.getlength(text) return y + max(line_height(f) for _, f in rs) def draw_rich(draw, cx, y, segments, max_width, leading): """Word-wrap `segments` [(text, font, color), ...] into centered lines.""" words = [] for text, f, color in segments: words += [(w, f, color) for w in text.split()] lines, line, width = [], [], 0.0 for w, f, color in words: space = f.getlength(" ") if line else 0 adv = f.getlength(w) if line and width + space + adv > max_width * SS: lines.append(line) line, width = [(w, f, color)], adv else: line.append((w, f, color)) width += space + adv if line: lines.append(line) for ln in lines: total = sum(f.getlength(w) for w, f, _ in ln) total += sum(f.getlength(" ") for _, f, _ in ln[:-1]) x = cx - total / 2 for i, (w, f, color) in enumerate(ln): draw.text((x, y), w, font=f, fill=color, anchor="la") x += f.getlength(w) + (f.getlength(" ") if i < len(ln) - 1 else 0) y += leading * SS return y def clean_name(name, limit=64): """Collapse whitespace and cap length so the layout can't be blown up.""" name = " ".join(str(name or "").split()) return name[:limit].rstrip() if len(name) > limit else name # --- renderer ------------------------------------------------------------- def render_certificate(participant_name, stats_line, out_path): name = clean_name(participant_name) or "Anonymous participant" img = Image.new("RGB", (W * SS, H * SS), PAPER) d = ImageDraw.Draw(img) cx = W * SS // 2 for x in range(0, W + 1, 40): d.line([(x * SS, 0), (x * SS, H * SS)], fill=GRID, width=SS) for y in range(0, H + 1, 40): d.line([(0, y * SS), (W * SS, y * SS)], fill=GRID, width=SS) d.rounded_rectangle([60 * SS, 60 * SS, (W - 60) * SS, (H - 60) * SS], radius=24 * SS, outline=INK, width=3 * SS) d.rounded_rectangle([134 * SS, 134 * SS, (W - 134) * SS, (H - 134) * SS], radius=16 * SS, outline=ORANGE, width=2 * SS) y = 232 * SS y = draw_tracked(d, cx, y, "REPRODUCING ICML 2026 · OPEN REPRODUCTIONS", font(MONO_BOLD, 24), ORANGE, 8) y += 26 * SS y = draw_centered(d, cx, y, "Certificate of Participation", fit_font(SERIF_BOLD, 92, "Certificate of Participation", 1640), INK) y += 40 * SS y = draw_centered(d, cx, y, "This certificate is proudly presented to", font(SANS, 32), MUTED) y += 30 * SS name_runs = fit_runs(name, SERIF_BOLD, 88, 1560, 40) if not name_runs: # every character was undrawable name_runs = fit_runs("Anonymous participant", SERIF_BOLD, 88, 1560, 40) y = draw_runs_centered(d, cx, y, name_runs, INK) y += 34 * SS d.rounded_rectangle([cx - 280 * SS, y, cx + 280 * SS, y + 3 * SS], radius=2 * SS, fill=ORANGE) y += 3 * SS y += 48 * SS body, body_bold = font(SANS, 32), font(SANS_BOLD, 32) y = draw_rich(d, cx, y, [ ("for contributing to the community reproduction of ICML 2026, with", body, INK), (stats_line, body_bold, ORANGE), ("independently confirmed by the automated Logbook Judge, as part of " "the largest open, claim-by-claim audit of a machine learning " "conference to date.", body, INK), ], max_width=1380, leading=50) y += 32 * SS draw_tracked(d, cx, y, "JULY 15 – AUGUST 2, 2026", font(MONO_BOLD, 24), MUTED, 5) # --- footer --- partners = [("Trackio", INK), ("·", SEP), ("Hugging Face", INK), ("·", SEP), ("alphaXiv", INK)] pf = font(SANS_BOLD, 30) gap = 34 * SS total = sum(pf.getlength(t) for t, _ in partners) + gap * (len(partners) - 1) x, py = cx - total / 2, (H - 268) * SS for text, color in partners: d.text((x, py), text, font=pf, fill=color, anchor="la") x += pf.getlength(text) + gap org = ("Issued by the ICML 2026 Agent Repro organizers · " "huggingface.co/ICML-2026-agent-repro") d.text((cx, (H - 202) * SS), org, font=font(SANS, 22), fill=MUTED, anchor="ma") img.resize((W, H), Image.LANCZOS).save(out_path, "PNG", optimize=True) return out_path if __name__ == "__main__": # local preview render_certificate("Ada Lovelace", "1673 claims verified across 347 papers", "preview.png")