HERO IMAGE: - Generated 3 concepts with gemini-3-pro-image-preview, picked #1 - Phone showing 'Payment Received' notification at a charity gala dinner - Warm tungsten bokeh chandeliers against dark bg-gray-950 - Directly visualizes the headline: 'money in the bank' - Candid documentary angle, not looking at camera, brand compliant IMAGE OPTIMIZATION (85% total reduction): - All 21 images resized: landscape max 1200px, portrait max 1000px - Compressed JPEG quality 80, progressive encoding, EXIF stripped - Total: 13.6MB -> 2.1MB (saved 11.5MB) - Individual savings: 81-90% per image NEXT.JS IMAGE PIPELINE: - Added sharp (10x faster than squoosh for image processing) - next.config.mjs: WebP format, proper device/image sizes, 1yr cache TTL - Dockerfile: libc6-compat + NEXT_SHARP_PATH for Alpine sharp support - First request: ~3s (processing), cached: <1s WebP served sizes: hero 52KB, cards 32-40KB (vs original 500-800KB JPEGs)
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
"""Optimize all landing images: resize + compress + strip EXIF.
|
|
Also set hero-concept-1.jpg as the new hero image."""
|
|
import sys, os, shutil
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
from PIL import Image
|
|
|
|
IMG_DIR = "pledge-now-pay-later/public/images/landing"
|
|
BRAND_DIR = "pledge-now-pay-later/brand/photography"
|
|
|
|
# Max dimensions per image type
|
|
# Portrait (4:5 or 1:1) → max 1000px long side
|
|
# Landscape (16:9) → max 1200px wide
|
|
MAX_LANDSCAPE = 1200
|
|
MAX_PORTRAIT = 1000
|
|
QUALITY = 80
|
|
|
|
def optimize(path, max_long_side):
|
|
"""Resize + compress + strip EXIF. Returns (old_size, new_size)."""
|
|
old_size = os.path.getsize(path)
|
|
img = Image.open(path)
|
|
|
|
# Strip EXIF by creating new image
|
|
if img.mode != "RGB":
|
|
img = img.convert("RGB")
|
|
|
|
# Resize if larger than max
|
|
w, h = img.size
|
|
long_side = max(w, h)
|
|
if long_side > max_long_side:
|
|
ratio = max_long_side / long_side
|
|
new_w = int(w * ratio)
|
|
new_h = int(h * ratio)
|
|
img = img.resize((new_w, new_h), Image.LANCZOS)
|
|
|
|
# Save with progressive JPEG, quality 80, optimized
|
|
img.save(path, "JPEG", quality=QUALITY, optimize=True, progressive=True)
|
|
new_size = os.path.getsize(path)
|
|
return old_size, new_size, img.size
|
|
|
|
# Step 1: Set hero-concept-1 as the main hero
|
|
hero_src = os.path.join(IMG_DIR, "hero-concept-1.jpg")
|
|
hero_dst = os.path.join(IMG_DIR, "00-hero.jpg")
|
|
if os.path.exists(hero_src):
|
|
shutil.copy2(hero_src, hero_dst)
|
|
# Also copy to brand dir
|
|
shutil.copy2(hero_src, os.path.join(BRAND_DIR, "00-hero.jpg"))
|
|
print(f"Set hero: hero-concept-1.jpg -> 00-hero.jpg")
|
|
|
|
# Step 2: Clean up concept images
|
|
for f in ["hero-concept-1.jpg", "hero-concept-2.jpg", "hero-concept-3.jpg"]:
|
|
p = os.path.join(IMG_DIR, f)
|
|
if os.path.exists(p):
|
|
os.remove(p)
|
|
print(f"Cleaned: {f}")
|
|
|
|
# Step 3: Optimize all images
|
|
total_old = 0
|
|
total_new = 0
|
|
|
|
for fname in sorted(os.listdir(IMG_DIR)):
|
|
if not fname.endswith(".jpg"):
|
|
continue
|
|
path = os.path.join(IMG_DIR, fname)
|
|
img = Image.open(path)
|
|
w, h = img.size
|
|
img.close()
|
|
|
|
# Determine max size based on orientation
|
|
if w > h:
|
|
max_side = MAX_LANDSCAPE # landscape
|
|
else:
|
|
max_side = MAX_PORTRAIT # portrait or square
|
|
|
|
old_size, new_size, final_dims = optimize(path, max_side)
|
|
total_old += old_size
|
|
total_new += new_size
|
|
saved_pct = (1 - new_size / old_size) * 100 if old_size > 0 else 0
|
|
print(f" {fname:45s} {final_dims[0]:>5d}x{final_dims[1]:<5d} {old_size//1024:>4d}KB -> {new_size//1024:>4d}KB ({saved_pct:+.0f}%)")
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"Total: {total_old//1024}KB -> {total_new//1024}KB ({(1 - total_new/total_old)*100:.0f}% reduction)")
|
|
print(f"{'='*60}")
|