"""Generate synthetic fintech UI screenshots containing dark-pattern copy. The screenshots are plain rendered UI with real text so Tesseract can read them and the classifier has something genuine to detect. Run once: python generate_samples.py Outputs PNGs into ./samples/ (served by Flask at /api/samples/). """ from PIL import Image, ImageDraw, ImageFont import os OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples") os.makedirs(OUT_DIR, exist_ok=True) W, H = 900, 640 BG = (248, 249, 252) CARD = (255, 255, 255) INK = (24, 28, 40) MUTED = (110, 118, 135) ACCENT = (79, 70, 229) DANGER = (220, 53, 69) WARN_BG = (255, 243, 205) WARN_INK = (133, 100, 4) GREEN = (25, 135, 84) def font(size, bold=False): names = ["arialbd.ttf"] if bold else ["arial.ttf"] for name in names + ["DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf"]: try: return ImageFont.truetype(name, size) except OSError: continue return ImageFont.load_default() def card(d, x0, y0, x1, y1, fill=CARD, outline=(225, 228, 235)): d.rounded_rectangle([x0, y0, x1, y1], radius=12, fill=fill, outline=outline, width=1) def button(d, x0, y0, x1, y1, label, fill=ACCENT, ink=(255, 255, 255), size=20, bold=True): d.rounded_rectangle([x0, y0, x1, y1], radius=8, fill=fill) f = font(size, bold) tw = d.textlength(label, font=f) d.text(((x0 + x1 - tw) / 2, (y0 + y1) / 2 - size / 2 - 2), label, font=f, fill=ink) def make_checkout(): img = Image.new("RGB", (W, H), BG) d = ImageDraw.Draw(img) d.rectangle([0, 0, W, 64], fill=(30, 34, 50)) d.text((30, 20), "PayVault | Secure Checkout", font=font(22, True), fill=(255, 255, 255)) card(d, 30, 90, 560, 600) d.text((55, 110), "Premium Trading Plan", font=font(26, True), fill=INK) d.text((55, 150), "$29.99 / month", font=font(22), fill=MUTED) d.rounded_rectangle([55, 195, 535, 245], radius=8, fill=WARN_BG) d.text((70, 207), "Hurry! Offer expires in 04:59 minutes", font=font(20, True), fill=WARN_INK) d.rounded_rectangle([55, 260, 535, 310], radius=8, fill=(253, 232, 232)) d.text((70, 272), "Only 2 spots left at this price!", font=font(20, True), fill=DANGER) d.text((55, 335), "17 people are viewing this offer right now", font=font(18), fill=MUTED) d.text((55, 370), "1,358 traders upgraded this week", font=font(18), fill=MUTED) button(d, 55, 430, 535, 485, "CLAIM MY DISCOUNT NOW") d.text((150, 510), "No thanks, I prefer losing money", font=font(15), fill=(180, 184, 195)) card(d, 590, 90, 870, 600) d.text((615, 110), "Order Summary", font=font(20, True), fill=INK) d.text((615, 155), "Plan", font=font(16), fill=MUTED) d.text((800, 155), "$29.99", font=font(16), fill=INK) d.text((615, 185), "Priority support", font=font(16), fill=MUTED) d.text((800, 185), "$4.99", font=font(16), fill=INK) d.line([615, 225, 845, 225], fill=(225, 228, 235), width=1) d.text((615, 240), "Total today", font=font(18, True), fill=INK) d.text((790, 240), "$34.98", font=font(18, True), fill=INK) img.save(os.path.join(OUT_DIR, "checkout_urgency.png")) def make_cancellation(): img = Image.new("RGB", (W, H), BG) d = ImageDraw.Draw(img) d.rectangle([0, 0, W, 64], fill=(30, 34, 50)) d.text((30, 20), "CoinNest | Account Settings", font=font(22, True), fill=(255, 255, 255)) card(d, 150, 110, 750, 560) d.text((185, 140), "Wait! Before you go...", font=font(28, True), fill=INK) d.text((185, 195), "Are you sure you want to cancel? You will lose all", font=font(19), fill=INK) d.text((185, 225), "your rewards and your exclusive member benefits", font=font(19), fill=INK) d.text((185, 255), "will be gone forever.", font=font(19), fill=INK) d.text((185, 305), "93% of members regret cancelling within a month", font=font(17), fill=MUTED) button(d, 185, 360, 715, 415, "KEEP MY BENEFITS", fill=GREEN) button(d, 185, 430, 715, 480, "Remind me later", fill=(235, 237, 242), ink=MUTED, bold=False) d.text((340, 505), "continue to cancellation step 1 of 6", font=font(13), fill=(195, 198, 207)) img.save(os.path.join(OUT_DIR, "cancel_confirmshaming.png")) def make_signup(): img = Image.new("RGB", (W, H), BG) d = ImageDraw.Draw(img) d.rectangle([0, 0, W, 64], fill=(30, 34, 50)) d.text((30, 20), "LoanLeap | Create Account", font=font(22, True), fill=(255, 255, 255)) card(d, 120, 100, 780, 580) d.text((155, 125), "You're almost done!", font=font(26, True), fill=INK) def checkbox(y, label, sub, checked=True): d.rounded_rectangle([155, y, 179, y + 24], radius=5, fill=ACCENT if checked else CARD, outline=(200, 203, 212), width=1) if checked: d.line([160, y + 12, 166, y + 18], fill=(255, 255, 255), width=3) d.line([166, y + 18, 175, y + 6], fill=(255, 255, 255), width=3) d.text((195, y), label, font=font(18, True), fill=INK) d.text((195, y + 28), sub, font=font(15), fill=MUTED) checkbox(180, "Add Payment Protection Plus (+$9.99/mo)", "Automatically added to protect your transactions") checkbox(255, "Enroll in Premium Credit Monitoring (+$14.99/mo)", "Free trial converts to paid plan after 7 days") checkbox(330, "Share my data with trusted marketing partners", "By continuing you agree to receive personalised offers") d.text((155, 410), "By clicking Continue you accept the Terms, the Fee Schedule,", font=font(14), fill=MUTED) d.text((155, 432), "and authorise recurring charges to your saved card.", font=font(14), fill=MUTED) button(d, 155, 475, 745, 530, "CONTINUE") img.save(os.path.join(OUT_DIR, "signup_sneaking.png")) if __name__ == "__main__": make_checkout() make_cancellation() make_signup() print(f"Saved 3 sample screenshots to {OUT_DIR}")