- brand/make_screenshots.py frames raw shots at 1600x900 with captions + alt text - brand/LISTING.md: introduction, details, features, search terms, testing steps - bookings filters no longer overflow; footer reads '1 booking'; no empty pager Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.5 KiB
Python
107 lines
4.5 KiB
Python
"""Turns raw screenshots into 1600x900 Shopify App Store listing images in the Datewise brand.
|
|
|
|
Usage: python -I brand/make_screenshots.py
|
|
Input: brand/screenshots/raw/<n>-<slug>.(png|jpg) (browser screenshots; CROP below trims each one)
|
|
Output: brand/screenshots/listing/<n>-<slug>.png + alt-text.txt
|
|
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
|
|
|
|
TEAL_DEEP = (4, 90, 81) # #045A51
|
|
TEAL = (13, 107, 93) # #0D6B5D
|
|
SAGE = (232, 242, 237) # #E8F2ED
|
|
WASH = (20, 125, 110)
|
|
|
|
# n: (title, subtitle, alt text)
|
|
SHOTS = {
|
|
1: ("Customers choose a time in the cart",
|
|
"Store pickup, local delivery or shipping, then a date and a time slot",
|
|
"Shopify cart page with the Datewise scheduler: store pickup selected, a calendar, time slots and a confirmation showing the time held"),
|
|
2: ("Today's bookings at a glance",
|
|
"Bookings, capacity for the next 14 days and upcoming festival dates",
|
|
"Datewise Today page in the Shopify admin with booking counts and a 14-day capacity chart for a pickup location"),
|
|
3: ("Hours, slots and limits for each location",
|
|
"Preparation time, cutoffs, bookable days and orders per slot",
|
|
"Location settings with pickup on, slot length, preparation time, same-day cutoff and orders per slot"),
|
|
4: ("Every booking in one list",
|
|
"Filter by date, method and location, move a booking or export CSV",
|
|
"Bookings list with filters and a confirmed store pickup booking"),
|
|
5: ("A valid time is required at checkout",
|
|
"Works on every Shopify plan, including Buy it now and express checkouts",
|
|
"Datewise settings with the checkout guard on, the date and time shown at checkout, and payment rules"),
|
|
6: ("The chosen time stays with the order",
|
|
"Shown on the delivery line, in the order details and as order tags",
|
|
"Shopify order showing the pickup line with the chosen date and time, booking details and Datewise tags"),
|
|
}
|
|
|
|
FONT = Path.home() / "AppData/Local/Microsoft/Windows/Fonts/Inter-VariableFont_slnt,wght.ttf"
|
|
|
|
|
|
def font(size, weight=400):
|
|
try:
|
|
f = ImageFont.truetype(str(FONT), size)
|
|
try:
|
|
axes = f.get_variation_axes()
|
|
f.set_variation_by_axes([weight] + [0] * (len(axes) - 1))
|
|
except Exception:
|
|
pass
|
|
return f
|
|
except OSError:
|
|
return ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf" if weight >= 600 else "C:/Windows/Fonts/segoeui.ttf", size)
|
|
|
|
|
|
def background():
|
|
bg = Image.new("RGB", (W, H), TEAL_DEEP)
|
|
glow = Image.new("L", (W, H), 0)
|
|
ImageDraw.Draw(glow).ellipse((-520, -620, 900, 520), fill=80)
|
|
glow = glow.filter(ImageFilter.GaussianBlur(170))
|
|
return Image.composite(Image.new("RGB", (W, H), WASH), bg, glow)
|
|
|
|
|
|
def compose(raw_path, title, subtitle):
|
|
bg = background()
|
|
d = ImageDraw.Draw(bg)
|
|
d.text((80, 60), title, font=font(52, 700), fill=(255, 255, 255))
|
|
d.text((82, 132), subtitle, font=font(27, 450), fill=SAGE)
|
|
|
|
shot = Image.open(raw_path).convert("RGB")
|
|
max_w, max_h = W - 160, H - 230
|
|
scale = min(max_w / shot.width, max_h / shot.height)
|
|
shot = shot.resize((round(shot.width * scale), round(shot.height * scale)), Image.LANCZOS)
|
|
x, y = (W - shot.width) // 2, 200
|
|
# shadow + rounded frame
|
|
shadow = Image.new("L", (W, H), 0)
|
|
ImageDraw.Draw(shadow).rounded_rectangle((x + 6, y + 18, x + shot.width + 6, y + shot.height + 18), 18, fill=120)
|
|
shadow = shadow.filter(ImageFilter.GaussianBlur(24))
|
|
bg = Image.composite(Image.new("RGB", (W, H), (2, 40, 36)), bg, shadow)
|
|
mask = Image.new("L", shot.size, 0)
|
|
ImageDraw.Draw(mask).rounded_rectangle((0, 0, shot.width - 1, shot.height - 1), 16, fill=255)
|
|
bg.paste(shot, (x, y), mask)
|
|
return bg
|
|
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
alt_lines = []
|
|
for raw in sorted(RAW.glob("*.*")):
|
|
n = int(raw.name.split("-", 1)[0])
|
|
if n not in SHOTS:
|
|
continue
|
|
title, subtitle, alt = SHOTS[n]
|
|
out = OUT / (raw.stem + ".png")
|
|
compose(raw, title, subtitle).save(out, optimize=True)
|
|
alt_lines.append(f"{out.name}: {alt}")
|
|
print("wrote", out.name)
|
|
(OUT / "alt-text.txt").write_text("\n".join(alt_lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|