Files

97 lines
3.4 KiB
Python

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},
}