Files
kottgard-testing/scripts/branded-beef-boneless.py

130 lines
4.3 KiB
Python

#!/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()