Files
kottgard-production/scripts/process-all-site-images.py

213 lines
7.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()