130 lines
4.3 KiB
Python
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() |