Files
oikos/docs/generate_showcase.py
T
Ulas 8e01d4c749 Add theme-adaptive screenshots to README and fix manifest icons
Replace old screenshots with new mobile/tablet variants in light and dark
mode. README now uses <picture> elements with prefers-color-scheme so
screenshots automatically match the viewer's GitHub theme. Split manifest
icon purpose field into separate "any" and "maskable" entries per PWA spec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 00:42:24 +01:00

167 lines
5.9 KiB
Python

"""Generate a showcase image for Oikos with screenshots in a row."""
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import os
# --- Config ---
SCREENSHOTS_DIR = os.path.join(os.path.dirname(__file__), "screenshots")
ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "public", "assets")
OUTPUT_DIR = os.path.dirname(__file__)
CANVAS_WIDTH = 2400
BG_COLOR = "#F8FAFC" # Light gray background
ACCENT_COLOR = "#2563EB" # Blue accent matching the logo
TEXT_COLOR = "#1E293B" # Dark text
SUBTITLE_COLOR = "#64748B" # Gray subtitle
SCREENSHOT_ORDER = ["dashboard.png", "tasks.png", "calendar.png", "shopping.png", "meals.png"]
SCREENSHOT_HEIGHT = 700 # Target height for each screenshot
PADDING = 60 # Padding around edges
SCREENSHOT_GAP = 32 # Gap between screenshots
CORNER_RADIUS = 24 # Rounded corners for screenshots
SHADOW_OFFSET = 8
SHADOW_BLUR = 20
APP_NAME = "Oikos"
TAGLINE = "Dein Familienplaner für Zuhause"
# Font paths (Fedora)
FONT_BOLD = "/usr/share/fonts/julietaula-montserrat-fonts/Montserrat-Bold.otf"
FONT_REGULAR = "/usr/share/fonts/julietaula-montserrat-fonts/Montserrat-Regular.otf"
FONT_FALLBACK_BOLD = "/usr/share/fonts/google-carlito-fonts/Carlito-Bold.ttf"
FONT_FALLBACK_REG = "/usr/share/fonts/google-carlito-fonts/Carlito-Regular.ttf"
def load_font(bold=True, size=48):
"""Load best available font."""
paths = [FONT_BOLD, FONT_FALLBACK_BOLD] if bold else [FONT_REGULAR, FONT_FALLBACK_REG]
for path in paths:
try:
return ImageFont.truetype(path, size)
except (OSError, IOError):
continue
return ImageFont.load_default()
def add_rounded_corners(img, radius):
"""Add rounded corners to an image."""
mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle([(0, 0), img.size], radius=radius, fill=255)
result = img.copy()
result.putalpha(mask)
return result
def create_shadow(size, radius, offset=8, blur=20):
"""Create a drop shadow."""
shadow_size = (size[0] + blur * 4, size[1] + blur * 4)
shadow = Image.new("RGBA", shadow_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(shadow)
x_off = blur * 2
y_off = blur * 2 + offset
draw.rounded_rectangle(
[(x_off, y_off), (x_off + size[0], y_off + size[1])],
radius=radius,
fill=(0, 0, 0, 50),
)
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
return shadow
def main():
# Load and resize screenshots
screenshots = []
for name in SCREENSHOT_ORDER:
path = os.path.join(SCREENSHOTS_DIR, name)
img = Image.open(path).convert("RGBA")
ratio = SCREENSHOT_HEIGHT / img.height
new_width = int(img.width * ratio)
img = img.resize((new_width, SCREENSHOT_HEIGHT), Image.LANCZOS)
screenshots.append(img)
# Calculate layout
total_screenshots_width = sum(s.width for s in screenshots) + SCREENSHOT_GAP * (len(screenshots) - 1)
# Scale screenshots if they don't fit
available_width = CANVAS_WIDTH - PADDING * 2
if total_screenshots_width > available_width:
scale = available_width / total_screenshots_width
new_screenshots = []
for s in screenshots:
new_w = int(s.width * scale)
new_h = int(s.height * scale)
new_screenshots.append(s.resize((new_w, new_h), Image.LANCZOS))
screenshots = new_screenshots
total_screenshots_width = sum(s.width for s in screenshots) + SCREENSHOT_GAP * (len(screenshots) - 1)
screenshot_h = screenshots[0].height
# Header area: logo + text
logo_size = 72
title_font = load_font(bold=True, size=52)
tagline_font = load_font(bold=False, size=28)
header_height = max(logo_size, 52 + 28 + 8) # logo or text stack height
top_section = PADDING + header_height + 48 # padding + header + gap to screenshots
canvas_height = top_section + screenshot_h + PADDING + SHADOW_BLUR * 2
# Create canvas
canvas = Image.new("RGBA", (CANVAS_WIDTH, canvas_height), BG_COLOR)
draw = ImageDraw.Draw(canvas)
# --- Draw subtle gradient accent at top ---
for y in range(min(6, canvas_height)):
alpha = int(180 * (1 - y / 6))
draw.line([(0, y), (CANVAS_WIDTH, y)], fill=(37, 99, 235, alpha))
# --- Draw logo ---
logo_path = os.path.join(ASSETS_DIR, "oikos-icon-1024.png")
logo = Image.open(logo_path).convert("RGBA")
logo = logo.resize((logo_size, logo_size), Image.LANCZOS)
# Center header: logo + name + tagline
title_bbox = draw.textbbox((0, 0), APP_NAME, font=title_font)
title_w = title_bbox[2] - title_bbox[0]
tagline_bbox = draw.textbbox((0, 0), TAGLINE, font=tagline_font)
tagline_w = tagline_bbox[2] - tagline_bbox[0]
header_total_w = logo_size + 20 + max(title_w, tagline_w)
header_x = (CANVAS_WIDTH - header_total_w) // 2
header_y = PADDING
canvas.paste(logo, (header_x, header_y), logo)
text_x = header_x + logo_size + 20
draw.text((text_x, header_y - 4), APP_NAME, fill=TEXT_COLOR, font=title_font)
draw.text((text_x, header_y + 48), TAGLINE, fill=SUBTITLE_COLOR, font=tagline_font)
# --- Draw screenshots ---
start_x = (CANVAS_WIDTH - total_screenshots_width) // 2
x = start_x
y = top_section
for s in screenshots:
# Shadow
shadow = create_shadow(s.size, CORNER_RADIUS, SHADOW_OFFSET, SHADOW_BLUR)
sx = x - SHADOW_BLUR * 2
sy = y - SHADOW_BLUR * 2
canvas.paste(shadow, (sx, sy), shadow)
# Screenshot with rounded corners
rounded = add_rounded_corners(s, CORNER_RADIUS)
canvas.paste(rounded, (x, y), rounded)
x += s.width + SCREENSHOT_GAP
# Save
output_path = os.path.join(OUTPUT_DIR, "showcase.png")
final = canvas.convert("RGB")
final.save(output_path, "PNG", optimize=True)
print(f"Showcase image saved: {output_path}")
print(f"Size: {CANVAS_WIDTH}x{canvas_height}")
if __name__ == "__main__":
main()