|
| 1 | +"""Generate the Open Graph / social-card image for modern-python.org. |
| 2 | +
|
| 3 | +Layout C: centered wordmark, thin divider, domain — white on brand green. |
| 4 | +Run locally (needs the Futura font, present on macOS): |
| 5 | +
|
| 6 | + uv run --with pillow python scripts/gen_social_card.py |
| 7 | +
|
| 8 | +Output: docs/assets/social-card.png (1200x630). |
| 9 | +""" |
| 10 | + |
| 11 | +import math |
| 12 | +import pathlib |
| 13 | + |
| 14 | +from PIL import Image, ImageDraw, ImageFont |
| 15 | + |
| 16 | +WIDTH, HEIGHT = 1200, 630 |
| 17 | +GREEN = (53, 104, 82) # #356852, the brand color sampled from the logo |
| 18 | +WHITE = (255, 255, 255) |
| 19 | + |
| 20 | +FONT_PATH = "/System/Library/Fonts/Supplemental/Futura.ttc" |
| 21 | +FONT_INDEX = 0 # Futura Medium |
| 22 | + |
| 23 | +WORDMARK = "MODERN PYTHON" |
| 24 | +DOMAIN = "MODERN-PYTHON.ORG" |
| 25 | +OUT = pathlib.Path(__file__).resolve().parent.parent / "docs" / "assets" / "social-card.png" |
| 26 | + |
| 27 | + |
| 28 | +def render_tracked(text: str, font: ImageFont.FreeTypeFont, tracking: float, alpha: int) -> Image.Image: |
| 29 | + """Render `text` with manual letter-spacing into a tight RGBA image.""" |
| 30 | + advances = [font.getlength(ch) for ch in text] |
| 31 | + total_w = sum(advances) + tracking * (len(text) - 1) |
| 32 | + bbox = font.getbbox(text) |
| 33 | + top, height = bbox[1], bbox[3] - bbox[1] |
| 34 | + img = Image.new("RGBA", (math.ceil(total_w), height), (0, 0, 0, 0)) |
| 35 | + draw = ImageDraw.Draw(img) |
| 36 | + x = 0.0 |
| 37 | + for ch, adv in zip(text, advances): |
| 38 | + draw.text((x, -top), ch, font=font, fill=(*WHITE, alpha)) |
| 39 | + x += adv + tracking |
| 40 | + return img |
| 41 | + |
| 42 | + |
| 43 | +def fit_wordmark(max_width: int) -> Image.Image: |
| 44 | + """Pick the largest Futura size whose tracked wordmark fits max_width.""" |
| 45 | + for size in range(140, 40, -2): |
| 46 | + font = ImageFont.truetype(FONT_PATH, size, index=FONT_INDEX) |
| 47 | + img = render_tracked(WORDMARK, font, tracking=size * 0.14, alpha=255) |
| 48 | + if img.width <= max_width: |
| 49 | + return img |
| 50 | + raise RuntimeError("wordmark never fit") |
| 51 | + |
| 52 | + |
| 53 | +def main() -> None: |
| 54 | + base = Image.new("RGBA", (WIDTH, HEIGHT), (*GREEN, 255)) |
| 55 | + |
| 56 | + wordmark = fit_wordmark(max_width=1000) |
| 57 | + domain_font = ImageFont.truetype(FONT_PATH, 30, index=FONT_INDEX) |
| 58 | + domain = render_tracked(DOMAIN, domain_font, tracking=30 * 0.16, alpha=217) # ~85% |
| 59 | + |
| 60 | + divider_w, divider_h = 360, 3 |
| 61 | + gap1, gap2 = 48, 42 |
| 62 | + group_h = wordmark.height + gap1 + divider_h + gap2 + domain.height |
| 63 | + y = (HEIGHT - group_h) // 2 |
| 64 | + |
| 65 | + base.alpha_composite(wordmark, ((WIDTH - wordmark.width) // 2, y)) |
| 66 | + y += wordmark.height + gap1 |
| 67 | + |
| 68 | + divider = Image.new("RGBA", (divider_w, divider_h), (*WHITE, 128)) # ~50% |
| 69 | + base.alpha_composite(divider, ((WIDTH - divider_w) // 2, y)) |
| 70 | + y += divider_h + gap2 |
| 71 | + |
| 72 | + base.alpha_composite(domain, ((WIDTH - domain.width) // 2, y)) |
| 73 | + |
| 74 | + OUT.parent.mkdir(parents=True, exist_ok=True) |
| 75 | + base.convert("RGB").save(OUT, "PNG") |
| 76 | + print(f"wrote {OUT} ({base.width}x{base.height})") |
| 77 | + |
| 78 | + |
| 79 | +if __name__ == "__main__": |
| 80 | + main() |
0 commit comments