|
1 | 1 | from PIL import Image, ImageDraw, ImageFont |
2 | | -import os |
| 2 | +import os, sys, json |
3 | 3 |
|
4 | 4 | W, H = 1080, 1920 |
5 | 5 |
|
6 | | -def make_slide(text, subtitle, filename): |
7 | | - img = Image.new('RGB', (W, H), (10, 10, 15)) |
8 | | - draw = ImageDraw.Draw(img) |
9 | | - |
| 6 | +def get_fonts(): |
10 | 7 | try: |
11 | | - title_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 72) |
12 | | - sub_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 42) |
13 | | - small_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 32) |
| 8 | + title = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 64) |
| 9 | + body = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 52) |
| 10 | + small = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 32) |
| 11 | + big = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 80) |
14 | 12 | except: |
15 | | - title_font = ImageFont.load_default() |
16 | | - sub_font = title_font |
17 | | - small_font = title_font |
18 | | - |
19 | | - # Logo |
20 | | - draw.ellipse([W//2-60, 120, W//2+60, 240], outline=(85,85,85), width=3) |
21 | | - draw.text((W//2, 180), "35", fill=(0,212,255), font=title_font, anchor="mm") |
22 | | - |
23 | | - # Title |
24 | | - y = 400 |
| 13 | + title = body = small = big = ImageFont.load_default() |
| 14 | + return title, body, small, big |
| 15 | + |
| 16 | +def wrap_text(draw, text, font, max_width): |
25 | 17 | words = text.split() |
26 | 18 | lines, current = [], "" |
27 | 19 | for word in words: |
28 | 20 | test = (current + " " + word).strip() |
29 | | - bbox = draw.textbbox((0,0), test, font=title_font) |
30 | | - if bbox[2] > W - 120: |
31 | | - lines.append(current) |
| 21 | + bbox = draw.textbbox((0,0), test, font=font) |
| 22 | + if bbox[2] > max_width: |
| 23 | + if current: lines.append(current) |
32 | 24 | current = word |
33 | 25 | else: |
34 | 26 | current = test |
35 | 27 | if current: lines.append(current) |
36 | | - |
37 | | - for line in lines: |
38 | | - draw.text((W//2, y), line, fill=(255,255,255), font=title_font, anchor="mt") |
39 | | - y += 90 |
40 | | - |
41 | | - # Subtitle |
42 | | - y += 50 |
43 | | - for sub_line in subtitle.split("\n"): |
44 | | - words = sub_line.split() |
45 | | - sub_lines, current = [], "" |
46 | | - for word in words: |
47 | | - test = (current + " " + word).strip() |
48 | | - bbox = draw.textbbox((0,0), test, font=sub_font) |
49 | | - if bbox[2] > W - 100: |
50 | | - sub_lines.append(current) |
51 | | - current = word |
52 | | - else: |
53 | | - current = test |
54 | | - if current: sub_lines.append(current) |
55 | | - for line in sub_lines: |
56 | | - draw.text((W//2, y), line, fill=(150,150,150), font=sub_font, anchor="mt") |
57 | | - y += 55 |
58 | | - |
59 | | - draw.text((W//2, H-100), "email35.com", fill=(0,212,255), font=small_font, anchor="mm") |
60 | | - img.save(filename, "JPEG", quality=90) |
61 | | - print(f"Created {filename}") |
| 28 | + return lines |
62 | 29 |
|
63 | | -base = os.path.expanduser("~/email35/social") |
64 | | -os.makedirs(base, exist_ok=True) |
| 30 | +def make_slide(text, subtitle="", slide_num=0, total=0, is_hook=False, is_cta=False): |
| 31 | + img = Image.new('RGB', (W, H), (10, 10, 15)) |
| 32 | + draw = ImageDraw.Draw(img) |
| 33 | + title_font, body_font, small_font, big_font = get_fonts() |
| 34 | + |
| 35 | + # Logo at top |
| 36 | + draw.ellipse([W//2-40, 60, W//2+40, 140], outline=(85,85,85), width=2) |
| 37 | + draw.text((W//2, 100), "35", fill=(0,212,255), font=title_font, anchor="mm") |
| 38 | + |
| 39 | + if is_hook: |
| 40 | + # Hook slide — big text, eye-catching |
| 41 | + y = 500 |
| 42 | + lines = wrap_text(draw, text, big_font, W - 120) |
| 43 | + for line in lines: |
| 44 | + draw.text((W//2, y), line, fill=(255,255,255), font=big_font, anchor="mt") |
| 45 | + y += 100 |
| 46 | + if subtitle: |
| 47 | + y += 30 |
| 48 | + sub_lines = wrap_text(draw, subtitle, body_font, W - 100) |
| 49 | + for line in sub_lines: |
| 50 | + draw.text((W//2, y), line, fill=(200,200,200), font=body_font, anchor="mt") |
| 51 | + y += 65 |
| 52 | + elif is_cta: |
| 53 | + # CTA slide — cyan accent |
| 54 | + y = 600 |
| 55 | + lines = wrap_text(draw, text, title_font, W - 120) |
| 56 | + for line in lines: |
| 57 | + draw.text((W//2, y), line, fill=(0,212,255), font=title_font, anchor="mt") |
| 58 | + y += 80 |
| 59 | + if subtitle: |
| 60 | + y += 20 |
| 61 | + draw.text((W//2, y), subtitle, fill=(150,150,150), font=small_font, anchor="mt") |
| 62 | + else: |
| 63 | + # Content slide — body text, centered |
| 64 | + y = 600 |
| 65 | + lines = wrap_text(draw, text, body_font, W - 100) |
| 66 | + total_height = len(lines) * 65 |
| 67 | + y = (H - total_height) // 2 |
| 68 | + for line in lines: |
| 69 | + draw.text((W//2, y), line, fill=(230,230,240), font=body_font, anchor="mt") |
| 70 | + y += 65 |
| 71 | + |
| 72 | + # Slide counter |
| 73 | + if total > 0: |
| 74 | + draw.text((W//2, H-80), f"{slide_num}/{total}", fill=(80,80,80), font=small_font, anchor="mm") |
65 | 75 |
|
66 | | -make_slide("Your inbox should PAY YOU", "Every email you receive earns you money.\nSpam becomes impossible when sending\ncosts a penny.", f"{base}/slide1.jpg") |
67 | | -make_slide("Spam costs $0 to send. That's the problem.", "Email35 makes senders pay $0.01\nto reach you. Spammers can't\nafford to spam you.", f"{base}/slide2.jpg") |
68 | | -make_slide("How it works", "1. Get yourname@email35.com\n2. Share it everywhere\n3. Senders pay $0.01 to deliver\n4. Paid emails go to your real inbox", f"{base}/slide3.jpg") |
69 | | -make_slide("100 emails = $1/day = $365/year", "For doing nothing.\nYour attention has value.\nStop giving it away for free.", f"{base}/slide4.jpg") |
70 | | -make_slide("Get your free address now", "email35.com\nYour inbox, spam-free.", f"{base}/slide5.jpg") |
| 76 | + # CTA on last slide |
| 77 | + if is_cta: |
| 78 | + draw.text((W//2, H-80), "email35.com", fill=(0,212,255), font=small_font, anchor="mm") |
| 79 | + |
| 80 | + return img |
| 81 | + |
| 82 | +def generate_post(post_data, output_dir): |
| 83 | + """Generate slides from post data dict with 'slides' list.""" |
| 84 | + os.makedirs(output_dir, exist_ok=True) |
| 85 | + slides = post_data.get("slides", []) |
| 86 | + total = len(slides) |
| 87 | + |
| 88 | + for i, slide in enumerate(slides): |
| 89 | + is_hook = (i == 0) |
| 90 | + is_cta = (i == total - 1) |
| 91 | + text = slide.get("text", slide) if isinstance(slide, dict) else slide |
| 92 | + subtitle = slide.get("subtitle", "") if isinstance(slide, dict) else "" |
| 93 | + |
| 94 | + img = make_slide(text, subtitle, i+1, total, is_hook=is_hook, is_cta=is_cta) |
| 95 | + path = os.path.join(output_dir, f"slide{i+1}.jpg") |
| 96 | + img.save(path, "JPEG", quality=90) |
| 97 | + |
| 98 | + print(f"Generated {total} slides in {output_dir}") |
71 | 99 |
|
72 | | -print("All slides done!") |
| 100 | +# Default: generate from command line JSON or use built-in post |
| 101 | +if __name__ == "__main__": |
| 102 | + if len(sys.argv) > 1 and os.path.exists(sys.argv[1]): |
| 103 | + with open(sys.argv[1]) as f: |
| 104 | + post = json.load(f) |
| 105 | + generate_post(post, os.path.expanduser("~/email35/social")) |
| 106 | + else: |
| 107 | + # Default post — The Audacity |
| 108 | + post = { |
| 109 | + "slides": [ |
| 110 | + "You buy ONE thing from a company", |
| 111 | + "Their email team: noted. Deploying 47 emails about spatulas.", |
| 112 | + "Also spatula accessories. Spatula insurance. Spatula newsletter.", |
| 113 | + "You hit unsubscribe.", |
| 114 | + "Them: how about emails about FORKS instead", |
| 115 | + {"text": "What if every email cost them $0.01?", "subtitle": "Suddenly they'd learn restraint. email35.com"} |
| 116 | + ] |
| 117 | + } |
| 118 | + generate_post(post, os.path.expanduser("~/email35/social")) |
0 commit comments