from __future__ import annotations import base64 import hashlib from pathlib import Path import random import re from PIL import Image, ImageDraw, ImageFilter from publication_meta import CATEGORIES WIDTH = 1200 HEIGHT = 630 MAX_BYTES = 5 * 1024 * 1024 SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") _PALETTES = { "وكلاء": ((17, 25, 48), (82, 214, 186)), "نماذج": ((20, 22, 48), (129, 140, 248)), "استرجاع": ((13, 31, 43), (56, 189, 248)), "سلامة": ((37, 21, 35), (244, 114, 182)), "أدوات": ((16, 30, 35), (245, 174, 65)), "أعمال": ((31, 24, 20), (249, 115, 80)), } def _art(slug: str, category: str) -> Image.Image: seed = int.from_bytes(hashlib.sha256(f"{slug}|{category}".encode()).digest()[:8], "big") rng = random.Random(seed) background, accent = _PALETTES[category] image = Image.new("RGB", (WIDTH, HEIGHT), background) glow = Image.new("RGBA", image.size, (0, 0, 0, 0)) glow_draw = ImageDraw.Draw(glow, "RGBA") for _ in range(5): x = rng.randint(80, WIDTH - 80) y = rng.randint(60, HEIGHT - 60) radius = rng.randint(130, 320) glow_draw.ellipse( (x - radius, y - radius, x + radius, y + radius), fill=(*accent, rng.randint(18, 42)), ) glow = glow.filter(ImageFilter.GaussianBlur(70)) image = Image.alpha_composite(image.convert("RGBA"), glow) overlay = Image.new("RGBA", image.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay, "RGBA") points = [(rng.randint(110, WIDTH - 110), rng.randint(90, HEIGHT - 90)) for _ in range(12)] for index, start in enumerate(points): end = points[(index * 5 + 3) % len(points)] draw.line((start, end), fill=(*accent, 95), width=3) for index, (x, y) in enumerate(points): radius = 8 + (index % 4) * 4 draw.ellipse( (x - radius, y - radius, x + radius, y + radius), fill=(*accent, 220), outline=(235, 241, 247, 180), width=2, ) left = 68 top = 54 right = WIDTH - 68 bottom = HEIGHT - 54 draw.rounded_rectangle((left, top, right, bottom), radius=32, outline=(*accent, 120), width=3) draw.line((left + 35, bottom - 55, right - 35, bottom - 55), fill=(*accent, 155), width=4) return Image.alpha_composite(image, overlay).convert("RGB") def generate_cover(slug: str, category: str, output_dir: Path) -> dict: if not SLUG_RE.fullmatch(slug): raise ValueError("slug must be lowercase ASCII kebab-case") if category not in CATEGORIES: raise ValueError("category is invalid") output_dir.mkdir(parents=True, exist_ok=True) output = output_dir / f"{slug}.jpg" temporary = output.with_suffix(".jpg.tmp") image = _art(slug, category) image.save(temporary, format="JPEG", quality=91, optimize=True, progressive=True) if temporary.stat().st_size > MAX_BYTES: temporary.unlink(missing_ok=True) raise ValueError("generated cover exceeds 5MB") with Image.open(temporary) as check: if check.size != (WIDTH, HEIGHT) or check.format != "JPEG" or check.mode != "RGB": temporary.unlink(missing_ok=True) raise ValueError("generated cover failed validation") temporary.replace(output) encoded = base64.b64encode(output.read_bytes()).decode("ascii") return { "path": output, "coverImage": {"filename": output.name, "dataBase64": encoded}, }