Initialisation

This commit is contained in:
Baccar
2026-06-02 18:48:14 +04:00
commit 5efaf58707
43 changed files with 750 additions and 0 deletions

204
scripts/gen_covers.py Normal file
View 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}")