Files
Documents---Re/scripts/gen_covers.py
2026-06-02 18:48:14 +04:00

205 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Generate 1280x720 Gumroad cover images for the Basic Web Development Series.
Produces 24 PNG files: 17 individual product covers + 7 bundle covers.
Requirements: pip install Pillow
Run from any directory: python scripts/gen_covers.py
"""
from PIL import Image, ImageDraw, ImageFont
import os
OUT = r"C:\projects\Docs Re\images\covers"
os.makedirs(OUT, exist_ok=True)
W, H = 1280, 720
MARGIN = 80
FONT_BOLD = r"C:\Windows\Fonts\arialbd.ttf"
FONT_REG = r"C:\Windows\Fonts\arial.ttf"
def load_font(bold=False, size=32):
try:
return ImageFont.truetype(FONT_BOLD if bold else FONT_REG, size)
except OSError:
return ImageFont.load_default()
BRAND = {
"html": (228, 77, 38), # #E44D26
"css": ( 38, 77, 228), # #264DE4
"js": (184, 168, 0), # #B8A800
"green": ( 26, 122, 60), # #1A7A3C
"teal": ( 46, 134, 171), # #2E86AB
}
def _lighter(rgb, factor):
return tuple(min(255, int(c + (255 - c) * factor)) for c in rgb)
def _badge_colours(accent):
"""
Returns (badge_bg, badge_fg).
Yellow background gets a dark badge so text stays readable.
"""
r, g, b = accent
if r > 140 and g > 130 and b < 60: # yellow (#B8A800)
return (26, 26, 46), (r, g, b)
return (255, 255, 255), accent # white bg, accent text
def _rounded_rect(draw, x0, y0, x1, y1, radius, fill):
r = radius
draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill)
draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill)
for cx, cy in [(x0, y0), (x1 - 2*r, y0), (x0, y1 - 2*r), (x1 - 2*r, y1 - 2*r)]:
draw.ellipse([cx, cy, cx + 2*r, cy + 2*r], fill=fill)
def make_cover(filename, badge, title, subtitle, accent):
"""
badge short text for the top-left pill (e.g. 'HTML', 'CSS', 'SERIES')
title main title; use '\\n' for a two-line title
subtitle type label below the title
accent RGB tuple for background colour
"""
img = Image.new("RGB", (W, H), accent)
d = ImageDraw.Draw(img)
f_title = load_font(bold=True, size=54)
f_sub = load_font(bold=False, size=34)
f_series = load_font(bold=False, size=22)
f_badge = load_font(bold=True, size=40)
LINE_GAP = 8 # gap between title lines when title wraps
RULE_PAD = 22 # space between rule and first title line
GAP_T_S = 14 # gap between title block and subtitle
GAP_S_SR = 10 # gap between subtitle and series name
BOTTOM_PAD = 72 # padding below series name
SERIES_LABEL = "Basic Web Development Series"
def bb(text, fnt):
return d.textbbox((0, 0), text, font=fnt)
title_lines = title.split("\n")
t_bboxes = [bb(ln, f_title) for ln in title_lines]
t_heights = [b[3] - b[1] for b in t_bboxes]
total_title_h = sum(t_heights) + LINE_GAP * (len(t_heights) - 1)
sub_b = bb(subtitle, f_sub)
series_b = bb(SERIES_LABEL, f_series)
sub_h = sub_b[3] - sub_b[1]
series_h = series_b[3] - series_b[1]
content_h = RULE_PAD + total_title_h + GAP_T_S + sub_h + GAP_S_SR + series_h
rule_y = H - BOTTOM_PAD - content_h - 3
# ── Horizontal rule ────────────────────────────────────────────────────
rule_col = _lighter(accent, 0.55)
d.rectangle([MARGIN, rule_y, W - MARGIN, rule_y + 3], fill=rule_col)
# ── Title ──────────────────────────────────────────────────────────────
y = rule_y + RULE_PAD
for line, lh, line_bb in zip(title_lines, t_heights, t_bboxes):
d.text((MARGIN - line_bb[0], y - line_bb[1]), line,
font=f_title, fill=(255, 255, 255))
y += lh + LINE_GAP
y -= LINE_GAP # remove trailing gap after last line
# ── Subtitle ───────────────────────────────────────────────────────────
y += GAP_T_S
sub_bb2 = bb(subtitle, f_sub)
d.text((MARGIN - sub_bb2[0], y - sub_bb2[1]), subtitle,
font=f_sub, fill=(255, 255, 255))
y += sub_h
# ── Series name ────────────────────────────────────────────────────────
y += GAP_S_SR
series_col = _lighter(accent, 0.45)
sr_bb = bb(SERIES_LABEL, f_series)
d.text((MARGIN - sr_bb[0], y - sr_bb[1]), SERIES_LABEL,
font=f_series, fill=series_col)
# ── Badge (top-left) ───────────────────────────────────────────────────
badge_bg, badge_fg = _badge_colours(accent)
b_bb = bb(badge, f_badge)
bw, bh = b_bb[2] - b_bb[0], b_bb[3] - b_bb[1]
px, py = 28, 14
bx0, by0 = MARGIN, 58
bx1, by1 = bx0 + bw + 2*px, by0 + bh + 2*py
_rounded_rect(d, bx0, by0, bx1, by1, radius=14, fill=badge_bg)
d.text((bx0 + px - b_bb[0], by0 + py - b_bb[1]),
badge, font=f_badge, fill=badge_fg)
out_path = os.path.join(OUT, filename)
img.save(out_path)
print(f" {filename}")
# ── Run ────────────────────────────────────────────────────────────────────────
print("Generating covers...\n")
# Fundamentals — reference notes
make_cover("cover_html_fundamentals.png",
"HTML", "HTML Fundamentals", "Reference Guide", BRAND["html"])
make_cover("cover_css_fundamentals.png",
"CSS", "CSS Fundamentals", "Reference Guide", BRAND["css"])
make_cover("cover_js_fundamentals.png",
"JS", "JavaScript Fundamentals", "Reference Guide", BRAND["js"])
# Beginner exercises
make_cover("cover_html_beginner_exercises.png",
"HTML", "HTML Beginner Exercises", "Beginner Exercises", BRAND["html"])
make_cover("cover_html_beginner_answers.png",
"HTML", "HTML Beginner Answers", "Beginner Answer Sheet", BRAND["green"])
make_cover("cover_css_beginner_exercises.png",
"CSS", "CSS Beginner Exercises", "Beginner Exercises", BRAND["css"])
make_cover("cover_css_beginner_answers.png",
"CSS", "CSS Beginner Answers", "Beginner Answer Sheet", BRAND["green"])
make_cover("cover_js_beginner_exercises.png",
"JS", "JavaScript Beginner Exercises", "Beginner Exercises", BRAND["js"])
make_cover("cover_js_beginner_answers.png",
"JS", "JavaScript Beginner Answers", "Beginner Answer Sheet", BRAND["green"])
# Intermediate exercises
make_cover("cover_html_exercises.png",
"HTML", "HTML Exercises", "Intermediate Exercises", BRAND["html"])
make_cover("cover_html_answers.png",
"HTML", "HTML Answer Sheet", "Intermediate Answer Sheet", BRAND["green"])
make_cover("cover_css_exercises.png",
"CSS", "CSS Exercises", "Intermediate Exercises", BRAND["css"])
make_cover("cover_css_answers.png",
"CSS", "CSS Answer Sheet", "Intermediate Answer Sheet", BRAND["green"])
make_cover("cover_js_exercises.png",
"JS", "JavaScript Exercises", "Intermediate Exercises", BRAND["js"])
make_cover("cover_js_answers.png",
"JS", "JavaScript Answer Sheet", "Intermediate Answer Sheet", BRAND["green"])
# Strategy docs
make_cover("cover_monetising_guide.png",
"GUIDE", "Monetising Your\nProgramming Knowledge", "Strategy Guide", BRAND["teal"])
make_cover("cover_gumroad_strategy.png",
"GUIDE", "Gumroad Sales Strategy", "Sales Strategy", BRAND["teal"])
# Language bundles (single-language, use language colour)
make_cover("cover_bundle_html.png",
"HTML", "Complete HTML Pack", "5-Document Bundle", BRAND["html"])
make_cover("cover_bundle_css.png",
"CSS", "Complete CSS Pack", "5-Document Bundle", BRAND["css"])
make_cover("cover_bundle_js.png",
"JS", "Complete JavaScript Pack", "5-Document Bundle", BRAND["js"])
# Multi-language bundles (teal)
make_cover("cover_bundle_notes.png",
"NOTES", "Notes Bundle", "All 3 Fundamentals Guides", BRAND["teal"])
make_cover("cover_bundle_beginner.png",
"STARTER", "Beginner Exercises Bundle", "All 6 Beginner Documents", BRAND["teal"])
make_cover("cover_bundle_intermediate.png",
"LEVEL 2", "Intermediate Exercises Bundle", "All 6 Intermediate Documents", BRAND["teal"])
make_cover("cover_bundle_complete.png",
"SERIES", "Complete Series", "All 16 Teaching Documents", BRAND["teal"])
print(f"\nDone — 24 covers saved to:\n{OUT}")