Skip to content

Commit 8a4e5c6

Browse files
Add social preview image and generation script
1 parent 2de762b commit 8a4e5c6

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed

img/gen_social_preview.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#!/usr/bin/env python3
2+
"""Generate GitHub social preview image for CHSZLabLib (1280x640)."""
3+
4+
from PIL import Image, ImageDraw, ImageFont
5+
import math
6+
7+
W, H = 1280, 640
8+
9+
# Colors
10+
BG_DARK = (15, 23, 42) # slate-900
11+
BG_MID = (30, 41, 59) # slate-800
12+
ACCENT = (59, 130, 246) # blue-500
13+
ACCENT_LIGHT = (96, 165, 250) # blue-400
14+
WHITE = (255, 255, 255)
15+
GRAY = (148, 163, 184) # slate-400
16+
LIGHT_GRAY = (203, 213, 225) # slate-300
17+
GREEN = (34, 197, 94) # green-500
18+
PURPLE = (168, 85, 247) # purple-500
19+
ORANGE = (249, 115, 22) # orange-500
20+
CYAN = (6, 182, 212) # cyan-500
21+
22+
# Fonts
23+
font_bold_64 = ImageFont.truetype("/usr/share/fonts/opentype/inter/Inter-Bold.otf", 64)
24+
font_bold_48 = ImageFont.truetype("/usr/share/fonts/opentype/inter/Inter-Bold.otf", 48)
25+
font_semi_28 = ImageFont.truetype("/usr/share/fonts/opentype/inter/Inter-SemiBold.otf", 28)
26+
font_med_24 = ImageFont.truetype("/usr/share/fonts/opentype/inter/Inter-Medium.otf", 24)
27+
font_reg_22 = ImageFont.truetype("/usr/share/fonts/opentype/inter/Inter-Regular.otf", 22)
28+
font_mono_20 = ImageFont.truetype("/home/c_schulz/.local/share/fonts/FiraCodeNerdFont-Bold.ttf", 20)
29+
font_mono_18 = ImageFont.truetype("/home/c_schulz/.local/share/fonts/FiraCodeNerdFont-Bold.ttf", 18)
30+
31+
img = Image.new("RGB", (W, H), BG_DARK)
32+
draw = ImageDraw.Draw(img)
33+
34+
# Subtle gradient overlay (top to bottom)
35+
for y in range(H):
36+
alpha = y / H
37+
r = int(BG_DARK[0] * (1 - alpha * 0.3) + BG_MID[0] * (alpha * 0.3))
38+
g = int(BG_DARK[1] * (1 - alpha * 0.3) + BG_MID[1] * (alpha * 0.3))
39+
b = int(BG_DARK[2] * (1 - alpha * 0.3) + BG_MID[2] * (alpha * 0.3))
40+
draw.line([(0, y), (W, y)], fill=(r, g, b))
41+
42+
# Draw decorative graph nodes and edges in background
43+
nodes = [
44+
(95, 120, 8, ACCENT),
45+
(180, 80, 6, PURPLE),
46+
(140, 200, 7, CYAN),
47+
(250, 150, 5, GREEN),
48+
(60, 250, 6, ACCENT_LIGHT),
49+
(200, 280, 5, ORANGE),
50+
# Right side
51+
(1100, 100, 7, ACCENT),
52+
(1200, 160, 6, PURPLE),
53+
(1150, 250, 8, CYAN),
54+
(1050, 200, 5, GREEN),
55+
(1220, 80, 5, ACCENT_LIGHT),
56+
(1080, 310, 6, ORANGE),
57+
# Bottom corners
58+
(100, 500, 6, ACCENT),
59+
(200, 550, 5, PURPLE),
60+
(1150, 520, 7, CYAN),
61+
(1050, 570, 5, GREEN),
62+
(160, 580, 4, ACCENT_LIGHT),
63+
(1200, 560, 5, ORANGE),
64+
]
65+
66+
edges = [
67+
(0, 1), (0, 2), (1, 3), (2, 4), (2, 5), (3, 5), (4, 5),
68+
(6, 7), (6, 8), (7, 9), (8, 9), (6, 10), (8, 11), (9, 11),
69+
(12, 13), (13, 16), (12, 16),
70+
(14, 15), (14, 17), (15, 17),
71+
]
72+
73+
for i, j in edges:
74+
x1, y1 = nodes[i][0], nodes[i][1]
75+
x2, y2 = nodes[j][0], nodes[j][1]
76+
c = tuple(int(v * 0.25) for v in nodes[i][3])
77+
draw.line([(x1, y1), (x2, y2)], fill=c, width=2)
78+
79+
for x, y, r, color in nodes:
80+
faded = tuple(int(v * 0.4) for v in color)
81+
draw.ellipse([x - r, y - r, x + r, y + r], fill=faded)
82+
# inner bright dot
83+
r2 = max(2, r - 3)
84+
bright = tuple(min(255, int(v * 0.6)) for v in color)
85+
draw.ellipse([x - r2, y - r2, x + r2, y + r2], fill=bright)
86+
87+
# Accent line at top
88+
draw.rectangle([0, 0, W, 4], fill=ACCENT)
89+
90+
# Title — "CHSZ" in red, "LabLib" in white
91+
RED = (220, 38, 38)
92+
part1 = "CHSZ"
93+
part2 = "LabLib"
94+
bbox1 = draw.textbbox((0, 0), part1, font=font_bold_64)
95+
bbox2 = draw.textbbox((0, 0), part2, font=font_bold_64)
96+
tw = (bbox1[2] - bbox1[0]) + (bbox2[2] - bbox2[0])
97+
tx = (W - tw) // 2
98+
draw.text((tx, 100), part1, fill=RED, font=font_bold_64)
99+
tx += bbox1[2] - bbox1[0]
100+
draw.text((tx, 100), part2, fill=WHITE, font=font_bold_64)
101+
102+
# Subtitle
103+
sub = "State-of-the-art graph algorithms from C++ to Python"
104+
bbox = draw.textbbox((0, 0), sub, font=font_semi_28)
105+
sw = bbox[2] - bbox[0]
106+
draw.text(((W - sw) // 2, 185), sub, fill=LIGHT_GRAY, font=font_semi_28)
107+
108+
# Divider
109+
div_y = 235
110+
div_w = 200
111+
draw.line([(W // 2 - div_w, div_y), (W // 2 + div_w, div_y)], fill=ACCENT, width=2)
112+
113+
# Module boxes
114+
modules = [
115+
("Decomposition", "Partition, Cuts, Cluster", ACCENT),
116+
("Independence", "MIS, MWIS, HyperMIS", GREEN),
117+
("Orientation", "Edge Orientation", PURPLE),
118+
("Dynamic", "Dynamic Problems", ORANGE),
119+
]
120+
121+
box_w = 250
122+
box_h = 90
123+
gap = 30
124+
total_w = len(modules) * box_w + (len(modules) - 1) * gap
125+
start_x = (W - total_w) // 2
126+
box_y = 265
127+
128+
for i, (name, desc, color) in enumerate(modules):
129+
x = start_x + i * (box_w + gap)
130+
# Box background
131+
box_color = tuple(int(v * 0.15) + BG_DARK[j] for j, v in enumerate(color))
132+
draw.rounded_rectangle([x, box_y, x + box_w, box_y + box_h], radius=10, fill=box_color)
133+
# Border
134+
border_color = tuple(int(v * 0.5) for v in color)
135+
draw.rounded_rectangle([x, box_y, x + box_w, box_y + box_h], radius=10, outline=border_color, width=2)
136+
# Top accent
137+
draw.line([(x + 10, box_y + 1), (x + box_w - 10, box_y + 1)], fill=color, width=2)
138+
# Name
139+
bbox = draw.textbbox((0, 0), name, font=font_med_24)
140+
nw = bbox[2] - bbox[0]
141+
draw.text((x + (box_w - nw) // 2, box_y + 15), name, fill=WHITE, font=font_med_24)
142+
# Description
143+
bbox = draw.textbbox((0, 0), desc, font=font_reg_22)
144+
dw = bbox[2] - bbox[0]
145+
draw.text((x + (box_w - dw) // 2, box_y + 50), desc, fill=GRAY, font=font_reg_22)
146+
147+
# Code snippet
148+
code_y = 390
149+
code_bg = (22, 30, 48)
150+
code_h = 130
151+
code_w = 700
152+
code_x = (W - code_w) // 2
153+
draw.rounded_rectangle([code_x, code_y, code_x + code_w, code_y + code_h], radius=12, fill=code_bg)
154+
draw.rounded_rectangle([code_x, code_y, code_x + code_w, code_y + code_h], radius=12, outline=(51, 65, 85), width=1)
155+
156+
# Terminal dots
157+
for ci, col in enumerate([(239, 68, 68), (250, 204, 21), (34, 197, 94)]):
158+
draw.ellipse([code_x + 16 + ci * 20, code_y + 12, code_x + 28 + ci * 20, code_y + 24], fill=col)
159+
160+
lines = [
161+
("from", (198, 120, 221)), # keyword
162+
(" chszlablib ", LIGHT_GRAY),
163+
("import", (198, 120, 221)),
164+
(" Graph, Decomposition", (230, 192, 123)),
165+
]
166+
lx = code_x + 20
167+
ly = code_y + 40
168+
for text, color in lines:
169+
draw.text((lx, ly), text, fill=color, font=font_mono_18)
170+
bbox = draw.textbbox((0, 0), text, font=font_mono_18)
171+
lx += bbox[2] - bbox[0]
172+
173+
# Second line
174+
lx = code_x + 20
175+
ly = code_y + 68
176+
line2 = [
177+
("g = Graph(", LIGHT_GRAY),
178+
("n=100", (230, 192, 123)),
179+
(").add_edges(", LIGHT_GRAY),
180+
("edges", (152, 195, 121)),
181+
(").finalize()", LIGHT_GRAY),
182+
]
183+
for text, color in line2:
184+
draw.text((lx, ly), text, fill=color, font=font_mono_18)
185+
bbox = draw.textbbox((0, 0), text, font=font_mono_18)
186+
lx += bbox[2] - bbox[0]
187+
188+
# Third line
189+
lx = code_x + 20
190+
ly = code_y + 96
191+
line3 = [
192+
("result = Decomposition.partition(g, k=", LIGHT_GRAY),
193+
("4", (230, 192, 123)),
194+
(")", LIGHT_GRAY),
195+
]
196+
for text, color in line3:
197+
draw.text((lx, ly), text, fill=color, font=font_mono_18)
198+
bbox = draw.textbbox((0, 0), text, font=font_mono_18)
199+
lx += bbox[2] - bbox[0]
200+
201+
# Footer
202+
footer = "Algorithm Engineering Group, Heidelberg University"
203+
bbox = draw.textbbox((0, 0), footer, font=font_reg_22)
204+
fw = bbox[2] - bbox[0]
205+
206+
# Badges row — colors from README shields.io badges
207+
badges = ["Python 3.9+", "C++17", "pybind11"]
208+
badge_font = font_mono_20
209+
badge_gap = 20
210+
badge_colors = [
211+
(0x37, 0x76, 0xab), # #3776ab — Python badge
212+
(0x00, 0x59, 0x9C), # #00599C — C++ badge
213+
(0x06, 0x4F, 0x8C), # #064F8C — build/cmake badge
214+
]
215+
216+
# Measure total width
217+
total_badge_w = 0
218+
badge_dims = []
219+
for b in badges:
220+
bbox = draw.textbbox((0, 0), b, font=badge_font)
221+
bw = bbox[2] - bbox[0] + 24
222+
bh = 30
223+
badge_dims.append((bw, bh))
224+
total_badge_w += bw
225+
total_badge_w += (len(badges) - 1) * badge_gap
226+
227+
bx = (W - total_badge_w) // 2
228+
by = 548
229+
230+
for i, (b, (bw, bh)) in enumerate(zip(badges, badge_dims)):
231+
col = badge_colors[i]
232+
bg = tuple(int(v * 0.3) for v in col)
233+
draw.rounded_rectangle([bx, by, bx + bw, by + bh], radius=5, fill=bg)
234+
draw.rounded_rectangle([bx, by, bx + bw, by + bh], radius=5, outline=col, width=1)
235+
bbox = draw.textbbox((0, 0), b, font=badge_font)
236+
tw = bbox[2] - bbox[0]
237+
th = bbox[3] - bbox[1]
238+
draw.text((bx + (bw - tw) // 2, by + (bh - th) // 2 - 2), b, fill=WHITE, font=badge_font)
239+
bx += bw + badge_gap
240+
241+
# Footer text below badges
242+
draw.text(((W - fw) // 2, 588), footer, fill=GRAY, font=font_reg_22)
243+
244+
out = "/home/c_schulz/projects/coding/CHSZLabLib/social-preview.png"
245+
img.save(out, "PNG")
246+
print(f"Saved: {out}")
247+
print(f"Size: {img.size}")

img/social-preview.png

71.6 KB
Loading

0 commit comments

Comments
 (0)