#!/usr/bin/env python3 """Prepare a sharp, full-frame beef boneless product photo for the shop.""" from __future__ import annotations import argparse import urllib.request from pathlib import Path from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageStat ROOT = Path(__file__).resolve().parents[1] SITE = ROOT / "public" / "images" / "site" OUT_PATH = SITE / "beef-boneless.jpg" SOURCE_CACHE = SITE / "_beef_boneless_src.jpg" # High-res premium raw boneless beef (Unsplash) PRIMARY_URL = ( "https://images.unsplash.com/photo-1603048297172-c92544798d5a" "?auto=format&fit=crop&w=1800&h=1800&crop=center&q=95" ) # Original product reference (lower res fallback) FALLBACK_URL = ( "https://www.hotcurrymarket.fi/wp-content/uploads/2025/01/" "FRESH-BEEF-BONELESS-WITHOUT-FAT-1KG.jpg" ) OUTPUT_SIZE = (1400, 1400) def download(url: str, dest: Path) -> None: req = urllib.request.Request( url, headers={"User-Agent": "Mozilla/5.0 (compatible; Kottgard-site-builder/1.0)"}, ) with urllib.request.urlopen(req, timeout=60) as resp: dest.write_bytes(resp.read()) def trim_near_white(img: Image.Image, tolerance: int = 18) -> Image.Image: """Remove excess light borders from the source photo.""" rgb = img.convert("RGB") pixels = rgb.load() w, h = rgb.size def row_is_border(y: int) -> bool: samples = [pixels[x, y] for x in range(0, w, max(1, w // 24))] return all(min(p) >= 255 - tolerance for p in samples) def col_is_border(x: int) -> bool: samples = [pixels[x, y] for y in range(0, h, max(1, h // 24))] return all(min(p) >= 255 - tolerance for p in samples) top = 0 while top < h and row_is_border(top): top += 1 bottom = h - 1 while bottom > top and row_is_border(bottom): bottom -= 1 left = 0 while left < w and col_is_border(left): left += 1 right = w - 1 while right > left and col_is_border(right): right -= 1 if right - left > 80 and bottom - top > 80: return rgb.crop((left, top, right + 1, bottom + 1)) return rgb def enhance_photo(img: Image.Image) -> Image.Image: img = ImageOps.autocontrast(img, cutoff=0.5) img = ImageEnhance.Brightness(img).enhance(1.03) img = ImageEnhance.Contrast(img).enhance(1.08) img = ImageEnhance.Color(img).enhance(1.1) img = ImageEnhance.Sharpness(img).enhance(1.35) return img.filter(ImageFilter.UnsharpMask(radius=1.0, percent=110, threshold=2)) def crop_center_cover(img: Image.Image, size: tuple[int, int]) -> Image.Image: tw, th = size w, h = img.size scale = max(tw / w, th / h) nw, nh = int(w * scale), int(h * scale) resized = img.resize((nw, nh), Image.Resampling.LANCZOS) left = (nw - tw) // 2 top = (nh - th) // 2 return resized.crop((left, top, left + tw, top + th)) def prepare_product_image(source: Path, output: Path = OUT_PATH) -> None: beef = Image.open(source).convert("RGB") beef = trim_near_white(beef) beef = enhance_photo(beef) # Full-frame crop — meat fills the product image (no tiny subject in white void) beef = crop_center_cover(beef, OUTPUT_SIZE) beef.save(output, "JPEG", quality=96, optimize=True, subsampling=0) print(f"Saved product photo {OUTPUT_SIZE[0]}x{OUTPUT_SIZE[1]} → {output}") def main() -> None: parser = argparse.ArgumentParser(description="Prepare beef boneless product photo") parser.add_argument("--url", default=PRIMARY_URL) parser.add_argument("--input", type=Path) parser.add_argument("--output", type=Path, default=OUT_PATH) parser.add_argument("--use-cache", action="store_true") parser.add_argument("--fallback", action="store_true", help="Use hot curry source") args = parser.parse_args() if args.input: src = args.input elif args.use_cache and SOURCE_CACHE.exists(): src = SOURCE_CACHE print(f"Using cached source → {src}") else: url = FALLBACK_URL if args.fallback else args.url print(f"Downloading source image…\n {url}") try: download(url, SOURCE_CACHE) except Exception as exc: print(f"Primary download failed ({exc}), trying fallback…") download(FALLBACK_URL, SOURCE_CACHE) src = SOURCE_CACHE prepare_product_image(src, args.output) if __name__ == "__main__": main()