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:
parent
b59c2d0668
commit
731e0ba06a
118
brand/make_screenshots.py
Normal file
118
brand/make_screenshots.py
Normal 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()
|
||||
BIN
brand/screenshots/listing/6-home.png
Normal file
BIN
brand/screenshots/listing/6-home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
1
brand/screenshots/listing/alt-text.txt
Normal file
1
brand/screenshots/listing/alt-text.txt
Normal file
@ -0,0 +1 @@
|
||||
6-home.png: Offerlyst home page with setup checklist and recent bundle results
|
||||
BIN
brand/screenshots/raw/6-home.png
Normal file
BIN
brand/screenshots/raw/6-home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Loading…
x
Reference in New Issue
Block a user