#!/usr/bin/env python3 """Render a demo "photo" (JPEG) for attachments: an optional product image on a photo-like canvas with a caption bar and a camera timestamp. Used by seed-orders-returns.sh. make-demo-photo.py OUT.jpg [--image product.png] [--parcel] "Title" ["line 2" ["line 3"]] """ import random, sys, datetime from PIL import Image, ImageDraw, ImageFilter, ImageFont args = sys.argv[1:] out = args.pop(0) img_path = None; parcel = False while args and args[0].startswith("--"): a = args.pop(0) if a == "--image": img_path = args.pop(0) elif a == "--parcel": parcel = True lines = args or ["Demo"] W, H = 1200, 900 random.seed(out) bg = Image.new("RGB", (W, H), (226, 224, 220)) d = ImageDraw.Draw(bg) # soft vignette / table surface for i in range(0, H, 6): shade = 200 + int(26 * i / H) d.line([(0, i), (W, i)], fill=(shade, shade - 3, shade - 8), width=6) bg = bg.filter(ImageFilter.GaussianBlur(1.2)) d = ImageDraw.Draw(bg) def font(size, bold=True): for p in ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf"): try: return ImageFont.truetype(p, size) except Exception: pass return ImageFont.load_default() if parcel: # cardboard parcel with a shipping label x0, y0, x1, y1 = 180, 140, 1020, 660 d.rectangle([x0, y0, x1, y1], fill=(176, 132, 88), outline=(120, 86, 50), width=6) d.line([(x0, (y0 + y1) // 2), (x1, (y0 + y1) // 2)], fill=(140, 100, 60), width=5) d.rectangle([x0 + 60, y0 + 50, x0 + 520, y0 + 250], fill=(248, 248, 245), outline=(80, 80, 80), width=3) d.text((x0 + 80, y0 + 70), "RETOURE", font=font(44), fill=(30, 30, 30)) d.text((x0 + 80, y0 + 130), lines[0][:28], font=font(28, False), fill=(40, 40, 40)) for k in range(18): # barcode d.rectangle([x0 + 80 + k * 22, y0 + 180, x0 + 80 + k * 22 + random.choice((4, 8, 12)), y0 + 235], fill=(20, 20, 20)) d.rectangle([x0 + 600, y0 + 60, x1 - 60, y0 + 160], fill=(200, 40, 40)) d.text((x0 + 630, y0 + 85), "FRAGILE", font=font(40), fill=(255, 255, 255)) elif img_path: try: im = Image.open(img_path).convert("RGBA") im.thumbnail((720, 560)) shadow = Image.new("RGBA", (im.width + 40, im.height + 40), (0, 0, 0, 0)) ImageDraw.Draw(shadow).rounded_rectangle([20, 24, im.width + 20, im.height + 28], radius=24, fill=(0, 0, 0, 90)) shadow = shadow.filter(ImageFilter.GaussianBlur(14)) px, py = (W - im.width) // 2, 110 bg.paste(shadow, (px - 20, py - 20), shadow) bg.paste(im, (px, py), im) d = ImageDraw.Draw(bg) except Exception as e: d.text((80, 300), f"(Bild nicht ladbar: {e})", font=font(26, False), fill=(120, 40, 40)) # caption bar bar = Image.new("RGBA", (W, 170), (20, 20, 24, 200)) bg.paste(bar, (0, H - 170), bar) d = ImageDraw.Draw(bg) d.text((48, H - 150), lines[0][:60], font=font(40), fill=(255, 255, 255)) for i, l in enumerate(lines[1:3]): d.text((48, H - 100 + i * 42), l[:90], font=font(28, False), fill=(225, 225, 225)) stamp = datetime.datetime.now().strftime("%d.%m.%Y %H:%M") d.text((W - 300, 28), stamp, font=font(26, False), fill=(255, 200, 60)) # subtle grain noise = Image.effect_noise((W, H), 12).convert("L") bg = Image.blend(bg, Image.merge("RGB", (noise, noise, noise)), 0.05) bg.convert("RGB").save(out, "JPEG", quality=86) print(out)