"""Turns raw screenshots into 1600x900 Shopify App Store listing images in the Offerlyst brand. Usage: python brand/make_screenshots.py Input: brand/screenshots/raw/-.png (any size; crop out the browser bar first) Output: brand/screenshots/listing/-.png + alt-text.txt Captions and alt text come from SHOTS below, matched by the leading number. Rules (Shopify): 1600x900, no pricing, reviews or outcome claims on the image. """ from pathlib import Path from PIL import Image, ImageDraw, ImageFilter, ImageFont ROOT = Path(__file__).resolve().parent RAW = ROOT / "screenshots" / "raw" OUT = ROOT / "screenshots" / "listing" W, H = 1600, 900 DEEP_TEAL = (4, 91, 89) DARK_TEAL = (2, 52, 54) MINT = (44, 216, 193) SOFT_ICE = (244, 251, 250) SHOTS = { 1: ("Run every bundle deal at once", "Each item counts towards one offer, in the order you set", "Offerlyst offers list showing running bundle offers with status badges and priority order"), 2: ("Build a bundle in minutes", "Fixed bundles, mix & match or buy X get Y, with a live example cart", "Offer editor with bundle type, discount, products and a live example cart preview"), 3: ("A bundles page for your store", "Shoppers add the whole bundle to the cart in one click", "Storefront bundle page with product list, bundle price and Add bundle to cart button"), 4: ("Discounts apply automatically", "Native Shopify discount in cart and checkout, no codes", "Shopify cart and checkout showing the bundle discount applied automatically"), 5: ("Plan for every festival", "Templates and a calendar for Diwali, Eid, Christmas and more", "Festival templates and calendar with ready-made bundle offers"), 6: ("Set up in a few clicks", "A guided checklist gets your first offer live", "Offerlyst home page with setup checklist and recent bundle results"), } def font(size, bold=False): for name in (["seguisb.ttf", "segoeuib.ttf"] if bold else ["segoeui.ttf"]) + ["arial.ttf", "DejaVuSans.ttf"]: for base in ("C:/Windows/Fonts/", "/usr/share/fonts/truetype/dejavu/", ""): try: return ImageFont.truetype(base + name, size) except OSError: continue return ImageFont.load_default() def background(): bg = Image.new("RGB", (W, H), DEEP_TEAL) px = bg.load() for y in range(H): for x in range(0, W, 2): t = min(1.0, (x / W) * 0.55 + (y / H) * 0.6) c = tuple(int(DEEP_TEAL[i] * (1 - t) + DARK_TEAL[i] * t) for i in range(3)) px[x, y] = c if x + 1 < W: px[x + 1, y] = c # soft mint glow, top-left (echoes the icon) glow = Image.new("L", (W, H), 0) ImageDraw.Draw(glow).ellipse((-420, -520, 620, 360), fill=90) glow = glow.filter(ImageFilter.GaussianBlur(160)) bg = Image.composite(Image.new("RGB", (W, H), MINT), bg, glow) return bg def rounded(img, radius): mask = Image.new("L", img.size, 0) ImageDraw.Draw(mask).rounded_rectangle((0, 0, img.width - 1, img.height - 1), radius, fill=255) out = Image.new("RGBA", img.size, (0, 0, 0, 0)) out.paste(img, (0, 0), mask) return out def compose(raw_path, title, subtitle): bg = background().convert("RGBA") d = ImageDraw.Draw(bg) d.text((80, 64), title, font=font(52, bold=True), fill=(255, 255, 255)) d.text((80, 132), subtitle, font=font(28), fill=(200, 238, 232)) shot = Image.open(raw_path).convert("RGB") box_w, box_h = W - 160, H - 200 - 50 # area under the caption scale = min(box_w / shot.width, box_h / shot.height) shot = shot.resize((int(shot.width * scale), int(shot.height * scale)), Image.LANCZOS) x = (W - shot.width) // 2 y = 200 + (box_h - shot.height) // 2 shadow = Image.new("RGBA", (shot.width + 80, shot.height + 80), (0, 0, 0, 0)) ImageDraw.Draw(shadow).rounded_rectangle((40, 50, shot.width + 40, shot.height + 40), 18, fill=(0, 0, 0, 110)) shadow = shadow.filter(ImageFilter.GaussianBlur(18)) bg.alpha_composite(shadow, (x - 40, y - 40)) bg.alpha_composite(rounded(shot.convert("RGBA"), 14), (x, y)) return bg.convert("RGB") def main(): OUT.mkdir(parents=True, exist_ok=True) raws = sorted(p for p in RAW.glob("*") if p.suffix.lower() in (".png", ".jpg", ".jpeg")) if not raws: print(f"No screenshots in {RAW}. Name them like 1-offers.png, 2-editor.png ...") return alt_lines = [] for p in raws: try: n = int(p.stem.split("-")[0]) except ValueError: print(f"skip {p.name}: name must start with a number 1-6") continue title, subtitle, alt = SHOTS.get(n, ("Offerlyst", "", "Offerlyst screenshot")) out = OUT / f"{p.stem}.png" compose(p, title, subtitle).save(out, optimize=True) alt_lines.append(f"{out.name}: {alt}") print(f"made {out.relative_to(ROOT.parent)}") (OUT / "alt-text.txt").write_text("\n".join(alt_lines) + "\n", encoding="utf-8") print(f"alt text: {(OUT / 'alt-text.txt').relative_to(ROOT.parent)}") if __name__ == "__main__": main()