Skip to content

Commit 4ed45b2

Browse files
authored
Create generate_og.py
1 parent 7621b7e commit 4ed45b2

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

scripts/generate_og.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env python3
2+
"""
3+
OG card generator — with round avatar + bigger GitHub mark + bottom bar.
4+
Saves: social_preview.png
5+
"""
6+
7+
import os
8+
import argparse
9+
from PIL import Image, ImageDraw, ImageFont, ImageOps
10+
11+
FONT_REGULAR = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
12+
FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
13+
14+
def load_font(path, size):
15+
try:
16+
return ImageFont.truetype(path, size)
17+
except Exception:
18+
return ImageFont.load_default()
19+
20+
def measure(draw, text, font):
21+
try:
22+
b = draw.textbbox((0,0), text, font=font)
23+
return b[2]-b[0], b[3]-b[1]
24+
except Exception:
25+
try:
26+
return font.getsize(text)
27+
except Exception:
28+
return (len(text)*6, 20)
29+
30+
def wrap_text(text, font, max_w, draw):
31+
words = text.split()
32+
lines = []
33+
cur = ""
34+
for w in words:
35+
test = cur + (" " if cur else "") + w
36+
w_px, _ = measure(draw, test, font)
37+
if w_px <= max_w:
38+
cur = test
39+
else:
40+
if cur:
41+
lines.append(cur)
42+
cur = w
43+
if cur:
44+
lines.append(cur)
45+
return lines
46+
47+
def draw_stats(draw, x, y, font, color):
48+
items = [("Contributors", "1"), ("Issues", "0"), ("Stars", "0"), ("Forks", "0")]
49+
spacing = 64
50+
for label, count in items:
51+
text = f"{count} {label}"
52+
draw.text((x, y), text, font=font, fill=color)
53+
w, _ = measure(draw, text, font)
54+
x += w + spacing
55+
56+
def crop_circle(im):
57+
"""Returns a perfectly circular cropped version of the image."""
58+
w, h = im.size
59+
size = min(w, h)
60+
im = im.crop(((w - size) // 2, (h - size) // 2, (w + size) // 2, (h + size) // 2)) # square
61+
mask = Image.new("L", (size, size), 0)
62+
draw = ImageDraw.Draw(mask)
63+
draw.ellipse((0,0,size,size), fill=255)
64+
output = Image.new("RGBA", (size, size), (0,0,0,0))
65+
output.paste(im, (0,0), mask)
66+
return output
67+
68+
def draw_github_fallback(draw, gx, gy, size=48):
69+
"""Draw fallback GH icon if github-mark.png missing."""
70+
r = size // 6
71+
rect = [gx, gy, gx + size, gy + size]
72+
draw.rounded_rectangle(rect, radius=r, fill=(36, 41, 46))
73+
f = load_font(FONT_BOLD, size//2)
74+
tw, th = measure(draw, "GH", f)
75+
tx = gx + (size - tw)//2
76+
ty = gy + (size - th)//2
77+
draw.text((tx, ty), "GH", font=f, fill=(255,255,255))
78+
79+
def main():
80+
ap = argparse.ArgumentParser()
81+
ap.add_argument("--output", default="social_preview.png")
82+
ap.add_argument("--title", default="username/repo")
83+
ap.add_argument("--subtitle", default="A project description.")
84+
ap.add_argument("--author", default="")
85+
ap.add_argument("--sha", default="")
86+
ap.add_argument("--logo", default="assets/brand-logo.png")
87+
ap.add_argument("--github-mark", default="assets/github-mark.png")
88+
args = ap.parse_args()
89+
90+
W, H = 1280, 640
91+
BG = (255,255,255)
92+
TEXT = (28,32,36)
93+
SUB = (98,108,118)
94+
STATS = (100,110,124)
95+
96+
img = Image.new("RGB", (W,H), BG)
97+
draw = ImageDraw.Draw(img)
98+
99+
left = 100
100+
right = W - 260
101+
maxw = right - left
102+
103+
raw = args.title or "unknown/repo"
104+
if "/" in raw:
105+
owner, repo = raw.split("/", 1)
106+
else:
107+
owner, repo = "", raw
108+
109+
f_owner = load_font(FONT_REGULAR, 28)
110+
f_repo = load_font(FONT_BOLD, 64)
111+
f_desc = load_font(FONT_REGULAR, 26)
112+
f_stats = load_font(FONT_REGULAR, 22)
113+
114+
# Owner
115+
y = 120
116+
if owner:
117+
draw.text((left, y), f"{owner}/", font=f_owner, fill=SUB)
118+
_, oh = measure(draw, f"{owner}/", f_owner)
119+
y += oh + 10
120+
121+
# Repo (shrink as needed)
122+
rw, rh = measure(draw, repo, f_repo)
123+
if rw > maxw:
124+
for s in range(64, 28, -2):
125+
f_repo = load_font(FONT_BOLD, s)
126+
rw, rh = measure(draw, repo, f_repo)
127+
if rw <= maxw:
128+
break
129+
draw.text((left, y), repo, font=f_repo, fill=TEXT)
130+
y += rh + 18
131+
132+
# Description
133+
lines = wrap_text(args.subtitle, f_desc, maxw, draw)[:3]
134+
for line in lines:
135+
draw.text((left, y), line, font=f_desc, fill=SUB)
136+
_, lh = measure(draw, line, f_desc)
137+
y += lh + 6
138+
139+
# Stats
140+
y += 18
141+
draw_stats(draw, left, y, f_stats, STATS)
142+
143+
# Meta bottom-left
144+
meta = ""
145+
if args.author:
146+
meta = f"by {args.author}"
147+
if args.sha:
148+
meta += f" • {args.sha[:7]}"
149+
draw.text((left, H-64), meta, font=f_stats, fill=SUB)
150+
151+
# Round avatar (top-right)
152+
if os.path.exists(args.logo):
153+
try:
154+
avatar = Image.open(args.logo).convert("RGBA")
155+
avatar = crop_circle(avatar)
156+
avatar = avatar.resize((180,180), Image.LANCZOS)
157+
158+
# Optional white border
159+
border = ImageOps.expand(avatar, border=6, fill="white")
160+
161+
# Position
162+
ax = W - 260 + (260 - border.size[0])//2
163+
ay = 100
164+
img.paste(border, (ax, ay), border)
165+
166+
except Exception as e:
167+
print("Avatar error:", e)
168+
169+
# Bottom color bar
170+
bar_h = 18
171+
draw.rectangle([0, H-bar_h, int(W*0.6), H], fill=(232,76,61)) # red
172+
draw.rectangle([int(W*0.6), H-bar_h, W, H], fill=(44,111,180)) # blue
173+
174+
# Bigger GitHub icon
175+
gh_size = 48
176+
gx = W - 48 - gh_size
177+
gy = H - bar_h - gh_size - 12
178+
179+
if os.path.exists(args.github_mark):
180+
try:
181+
gh = Image.open(args.github_mark).convert("RGBA")
182+
gh.thumbnail((gh_size, gh_size))
183+
img.paste(gh, (gx, gy), gh)
184+
except:
185+
draw_github_fallback(draw, gx, gy, size=gh_size)
186+
else:
187+
draw_github_fallback(draw, gx, gy, size=gh_size)
188+
189+
# Save final image
190+
img.save(args.output, quality=95)
191+
print("Generated", args.output)
192+
193+
if __name__ == "__main__":
194+
main()

0 commit comments

Comments
 (0)