Add App Store screenshot generator (1600x900, brand frame, captions, alt text)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-14 12:10:00 -04:00
parent b59c2d0668
commit 731e0ba06a
4 changed files with 119 additions and 0 deletions

118
brand/make_screenshots.py Normal file
View File

@ -0,0 +1,118 @@
"""Turns raw screenshots into 1600x900 Shopify App Store listing images in the Offerlyst brand.
Usage: python brand/make_screenshots.py
Input: brand/screenshots/raw/<n>-<slug>.png (any size; crop out the browser bar first)
Output: brand/screenshots/listing/<n>-<slug>.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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View File

@ -0,0 +1 @@
6-home.png: Offerlyst home page with setup checklist and recent bundle results

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB