"""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/-.(png|jpg) (browser screenshots; CROP below trims each one) Output: brand/screenshots/listing/-.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 pickup or delivery line and in the order details", "Shopify order with the pickup line showing the chosen date and time, and the booking details"), } 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()