Initialisation
This commit is contained in:
204
scripts/gen_covers.py
Normal file
204
scripts/gen_covers.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
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}")
|
||||
Reference in New Issue
Block a user