""" 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}")