#!/usr/bin/env python3 """Generate the printable suburb posters from one template. Two orientations per suburb — A4 landscape and A4 portrait (vertical) — each carrying that suburb's reserved house referral code as a plaque, on two QR codes (one per audience), and across the tear-off tabs along the bottom. That code is the same one the weekly summary email tabulates. The generated .html files are fully self-contained (QR codes are inlined as SVG paths, the logo is inline SVG, fonts are web-safe), so they need nothing at view time. Print from a browser at A4 in the matching orientation, "Background graphics" ON, margins "None" — the page box already carries its own margin. Regenerating needs `segno` (QR encoding), which is NOT a project dependency: python3 -m pip install --target /tmp/pylibs segno PYTHONPATH=/tmp/pylibs python3 docs/posters/build_posters.py Adding a suburb: reserve its code (payments/migrations/0017_reserve_campaign_codes.py), add an entry to POSTERS below, and re-run. """ import pathlib from string import Template import segno BASE = "https://tutorsfirst.com.au" OUT_DIR = pathlib.Path(__file__).resolve().parent FEE = "9.90" # Site palette (templates/base.html :root) plus the lockup's lighter cell navy. # Three roles, kept strictly separate so the poster reads at a glance: # navy = structure (edge, masthead rule, the families card) # green = the tutor side, and the brand's "free/go" positives # red = the code, and only the code (plaque + tear-off tabs) NAVY = "#1f3a60" GREEN = "#1f7a44" SLATE = "#33507a" # the mark's non-TF cells, as in static/tf-lockup-og RED = "#c0392b" # the site's map-slider red INK = "#1a1a1a" GREY = "#6b7280" # the muted "(done fairly)" subtitle CREAM = "#f7efe1" # warm paper, carried over from the original poster RULE = "#d9cbb4" POSTERS = [ {"slug": "west-ryde-poster", "suburb": "West Ryde", "code": "WR2114"}, {"slug": "meadowbank-poster", "suburb": "Meadowbank", "code": "MB2114"}, ] # The 2x2 "confusion matrix" mark, in the same colours as static/tf-lockup-og: # a navy plate with three slate cells and the TF cell picked out in green. The # cell labels are dropped — at 14mm they'd be sub-millimetre and print as mud. LOGO_SVG = f"""""" FAMILY_BULLETS = f"""
  • Search & message tutors for free. Primary, NAPLAN, OC, Selective, HSC; by subject, level and suburb.
  • One ${FEE} connection fee, only when you want a tutor's contact details. No subscription.
  • Tutors are all ID and Qualification verified, with WWCC where appropriate.
  • One free connection on us with the code below, plus one more for every friend you refer. They stack.
  • """ TUTOR_BULLETS = """
  • List your ad free and keep 100% of your lesson fees. We never take a cut of what you teach.
  • Get found by families near you — search, map and suburb pages.
  • Earn trust with verified marks, ID and WWCC badges on your ad.
  • Refer a tutor and you both get a free week of Featured placement. Weeks stack.
  • """ def qr_svg(data, colour=NAVY, label=""): """A QR code as a compact, scalable inline SVG (one path, runs of dark modules merged per row). No border — the surrounding CSS supplies the quiet zone.""" qr = segno.make(data, error="m") matrix = list(qr.matrix) n = len(matrix) parts = [] for y, row in enumerate(matrix): x = 0 while x < n: if row[x]: run = 0 while x + run < n and row[x + run]: run += 1 parts.append(f"M{x} {y}h{run}v1h-{run}z") x += run else: x += 1 aria = f' role="img" aria-label="{label}"' if label else ' aria-hidden="true"' return (f'' f'') TEMPLATE = Template(""" tutorsfirst — $suburb poster ($code, $orient_name)
    $logo
    tutorsfirst
    An efficient & fair marketplace tutorsfirst.com.au

    Tutoring in $suburb(done fairly)

    Browse local, verified tutors for free. Tutors keep 100% of what they earn. No subscriptions, no lesson commissions.

    Looking for a tutor

      $family_bullets
    $qr_family
    Scan to register as a student or parent — the code is applied for you.

    Are you a tutor?

      $tutor_bullets
    $qr_tutor
    Scan to list your tutoring — free, reviewed within a business day.
    Use
    code
    $code
    Students & parents: your first connection is free.
    Tutors: a free week of Featured placement once you're approved.
    $tabs
    """) # Per-orientation geometry. Landscape sits the two audience cards side by side; # portrait stacks them and drops the headline kicker onto its own flush-right line. ORIENTATIONS = { "landscape": dict( suffix="", orient_css="landscape", orient_name="landscape", sheet_w="297mm", sheet_h="210mm", pad="10mm", wordmark_fs="30pt", h1_fs="33pt", li_fs="11.5pt", qr_size="23mm", code_fs="30pt", cols_tpl="1fr 1fr", cols_gap="5mm", n_tabs=14, kicker_fs="15pt", tabs_h="30mm", h1_flow="align-items: baseline; justify-content: space-between;", kicker_align="", ), "portrait": dict( suffix="-portrait", orient_css="portrait", orient_name="portrait (vertical)", sheet_w="210mm", sheet_h="297mm", pad="12mm", wordmark_fs="27pt", h1_fs="31pt", li_fs="11.5pt", qr_size="23mm", code_fs="32pt", cols_tpl="1fr", cols_gap="5mm", n_tabs=10, kicker_fs="15pt", tabs_h="28mm", # Portrait is too narrow to sit the aside beside the suburb, so it drops to # its own line, flush right. (Landscape shares the headline's baseline.) h1_flow="flex-direction: column;", kicker_align="align-self: flex-end;", ), } def build(poster, orient): o = ORIENTATIONS[orient] code, suburb = poster["code"], poster["suburb"] tab = (f'
    {code}' f'tutorsfirst.com.au
    ') html = TEMPLATE.substitute( suburb=suburb, code=code, logo=LOGO_SVG, navy=NAVY, green=GREEN, ink=INK, cream=CREAM, rule=RULE, red=RED, grey=GREY, slate=SLATE, family_bullets=FAMILY_BULLETS, tutor_bullets=TUTOR_BULLETS, qr_family=qr_svg(f"{BASE}/accounts/signup/student/?ref={code}", label=f"Register as a student or parent with code {code}"), qr_tutor=qr_svg(f"{BASE}/accounts/signup/tutor/?ref={code}", label=f"List as a tutor with code {code}"), tabs="\n ".join([tab] * o["n_tabs"]), **{k: v for k, v in o.items() if k not in ("suffix", "n_tabs")}, ) path = OUT_DIR / f"{poster['slug']}{o['suffix']}.html" path.write_text(html, encoding="utf-8") return path # Headless Chrome is the only renderer here that honours @page + print colours, # so the PDFs are produced from the very same HTML rather than a parallel layout. CHROME_CANDIDATES = [ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser", ] def _chrome(): import shutil for c in CHROME_CANDIDATES: if pathlib.Path(c).exists(): return c return shutil.which("google-chrome") or shutil.which("chromium") def to_pdf(html_path): """Print a poster to PDF beside its HTML. Returns None if Chrome isn't around.""" import subprocess chrome = _chrome() if chrome is None: return None pdf_path = html_path.with_suffix(".pdf") subprocess.run( [chrome, "--headless", "--disable-gpu", "--no-sandbox", "--no-pdf-header-footer", f"--print-to-pdf={pdf_path}", html_path.as_uri()], check=True, capture_output=True) return pdf_path if __name__ == "__main__": import sys want_pdf = "--no-pdf" not in sys.argv for p in POSTERS: for orient in ORIENTATIONS: path = build(p, orient) print("wrote", path) if want_pdf: pdf = to_pdf(path) print("wrote", pdf) if pdf else print( " (skipped PDF — no Chrome/Chromium found)")