#!/usr/bin/env python3 """Normalize site images: square canvas, HALAL badge + Kött Gård logo. Colors preserved.""" from __future__ import annotations import argparse import shutil from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT = Path(__file__).resolve().parents[1] SITE = ROOT / "public" / "images" / "site" BACKUP = SITE / "_originals" LOGO_PATH = SITE / "logo.jpeg" BRAND_NAME = "Kött Gård" SIZE = (1400, 1400) BG = (255, 255, 255) BURGUNDY = (139, 31, 31) GOLD = (212, 175, 55) SKIP = {"logo.jpeg", "README.txt"} SKIP_PATTERNS = (" copy", "-1.jpg") # duplicate uploads from user FILL_PRODUCT = 0.9 FILL_PAGE = 0.94 PAGE_IMAGES = {"hero.jpg", "about.jpg", "weekly-offers.jpg"} CATEGORY_IMAGES = { "category-chicken.jpg", "category-beef.jpg", "category-lamb.jpg", "category-fish.jpg", } def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: paths = [ "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Arial.ttf", "/Library/Fonts/Arial.ttf", ] for p in paths: try: return ImageFont.truetype(p, size) except OSError: continue return ImageFont.load_default() def preserve_colors(img: Image.Image) -> Image.Image: """Keep original photo pixels — no contrast, brightness, or color tweaks.""" return img.convert("RGB") def prep_logo(diameter: int) -> Image.Image: logo = Image.open(LOGO_PATH).convert("RGBA") w, h = logo.size side = min(w, h) logo = logo.crop(((w - side) // 2, (h - side) // 2, (w + side) // 2, (h + side) // 2)) logo = logo.resize((diameter, diameter), Image.Resampling.LANCZOS) mask = Image.new("L", (diameter, diameter), 0) ImageDraw.Draw(mask).ellipse((0, 0, diameter - 1, diameter - 1), fill=255) logo.putalpha(mask) return logo def draw_halal_badge(canvas: Image.Image, x: int, y: int, size: int = 130) -> None: layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(layer) draw.ellipse((x + 4, y + 5, x + size + 4, y + size + 5), fill=(0, 0, 0, 100)) draw.ellipse((x - 2, y - 2, x + size + 2, y + size + 2), fill=(*GOLD, 255)) draw.ellipse((x + 6, y + 6, x + size - 6, y + size - 6), fill=(255, 255, 255, 255)) draw.ellipse((x + 10, y + 10, x + size - 10, y + size - 10), outline=(*BURGUNDY, 255), width=4) f_ar = load_font(24, bold=True) f_halal = load_font(28, bold=True) f_sub = load_font(11, bold=True) ar = draw.textbbox((0, 0), "حلال", font=f_ar) draw.text((x + (size - ar[2] + ar[0]) // 2, y + 22), "حلال", fill=(*BURGUNDY, 255), font=f_ar) hl = draw.textbbox((0, 0), "HALAL", font=f_halal) draw.text((x + (size - hl[2] + hl[0]) // 2, y + 52), "HALAL", fill=(*BURGUNDY, 255), font=f_halal) ce = draw.textbbox((0, 0), "CERTIFIED", font=f_sub) draw.text((x + (size - ce[2] + ce[0]) // 2, y + 92), "CERTIFIED", fill=(*GOLD, 255), font=f_sub) canvas.alpha_composite(layer) def draw_logo_badge(canvas: Image.Image, logo: Image.Image, x: int, y: int) -> None: d = logo.size[0] layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(layer) draw.ellipse((x + 3, y + 4, x + d + 3, y + d + 4), fill=(0, 0, 0, 90)) draw.ellipse((x - 4, y - 4, x + d + 4, y + d + 4), fill=(255, 255, 255, 245), outline=(*GOLD, 255), width=3) canvas.alpha_composite(layer) canvas.alpha_composite(logo, (x, y)) def draw_brand_name(canvas: Image.Image, x: int, y: int) -> None: """Brand name pill beside the logo badge.""" layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(layer) font = load_font(22, bold=True) bbox = draw.textbbox((0, 0), BRAND_NAME, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] pad_x, pad_y = 14, 8 box = (x - tw - pad_x * 2, y, x - 8, y + th + pad_y * 2) draw.rounded_rectangle(box, radius=10, fill=(255, 255, 255, 235), outline=(*GOLD, 255), width=2) draw.text((box[0] + pad_x, box[1] + pad_y - 2), BRAND_NAME, fill=(*BURGUNDY, 255), font=font) canvas.alpha_composite(layer) def already_branded(path: Path) -> bool: """Skip images already at catalog size (user uploads with logos baked in).""" with Image.open(path) as img: return img.size == SIZE def should_skip(path: Path) -> bool: if path.name in SKIP or path.name.startswith("_"): return True return any(p in path.name for p in SKIP_PATTERNS) def compose(source: Path, dest: Path, logo: Image.Image) -> None: photo = preserve_colors(Image.open(source)) fill = FILL_PAGE if dest.name in PAGE_IMAGES else FILL_PRODUCT if dest.name in CATEGORY_IMAGES: fill = 0.92 canvas = Image.new("RGBA", SIZE, (*BG, 255)) max_side = int(SIZE[0] * fill) photo.thumbnail((max_side, max_side), Image.Resampling.LANCZOS) shadow = Image.new("RGBA", photo.size, (0, 0, 0, 0)) sh_draw = ImageDraw.Draw(shadow) sh_draw.rounded_rectangle( (8, 12, photo.width - 8, photo.height - 4), radius=18, fill=(0, 0, 0, 35), ) px = (SIZE[0] - photo.width) // 2 py = (SIZE[1] - photo.height) // 2 - 16 canvas.alpha_composite(shadow, (px, py + 6)) canvas.alpha_composite(photo.convert("RGBA"), (px, py)) margin = 28 draw_halal_badge(canvas, margin, margin) logo_size = 108 logo_x = SIZE[0] - logo_size - margin logo_y = SIZE[1] - logo_size - margin draw_logo_badge(canvas, logo, logo_x, logo_y) draw_brand_name(canvas, logo_x, logo_y + logo_size // 2 - 12) canvas.convert("RGB").save(dest, "JPEG", quality=94, optimize=True, subsampling=0) def ensure_catalog_files() -> None: pairs = [ ("beef-steak.jpg", "beef-steak-2.jpg"), ("beef-nihari-2.jpg", "beef-nihari.jpg"), ("category-lamb.jpg", "lamb-shoulder.jpg"), ] for target, source in pairs: t, s = SITE / target, SITE / source if not t.exists() and s.exists(): shutil.copy2(s, t) print(f"Created missing {target} from {source}") def main() -> None: parser = argparse.ArgumentParser(description="Brand site images (colors preserved)") parser.add_argument("--force", action="store_true", help="Re-process even if already 1400×1400") parser.add_argument("--only", nargs="*", help="Process only these filenames") args = parser.parse_args() if not LOGO_PATH.exists(): raise SystemExit(f"Missing logo: {LOGO_PATH}") BACKUP.mkdir(exist_ok=True) ensure_catalog_files() logo = prep_logo(108) files = sorted(SITE.glob("*.jpg")) processed = 0 skipped = 0 for path in files: if should_skip(path): continue if args.only and path.name not in args.only: continue if not args.force and already_branded(path): print(f"○ {path.name} (already branded, skipped)") skipped += 1 continue backup = BACKUP / path.name if not backup.exists(): shutil.copy2(path, backup) source = backup if backup.exists() else path compose(source, path, logo) print(f"✓ {path.name}") processed += 1 print(f"\nDone — {processed} processed, {skipped} skipped. Originals in {BACKUP}") if __name__ == "__main__": main()