#!/usr/bin/env python """Finalize a generated social image with exact brand formatting. This script keeps the AI-generated image and the company logo separate: generate the base image first, then overlay the real logo here so the brand mark does not get distorted by the image model. """ from __future__ import annotations import argparse from pathlib import Path try: from PIL import Image, ImageDraw, ImageOps except ModuleNotFoundError as exc: # pragma: no cover - friendly CLI error raise SystemExit( "Pillow is required. Install it with: " "python -m pip install Pillow" ) from exc def _open_rgba(path: Path) -> Image.Image: return Image.open(path).convert("RGBA") def _fit_cover(img: Image.Image, size: tuple[int, int]) -> Image.Image: target_w, target_h = size src_w, src_h = img.size scale = max(target_w / src_w, target_h / src_h) resized = img.resize( (round(src_w * scale), round(src_h * scale)), Image.Resampling.LANCZOS, ) left = (resized.width - target_w) // 2 top = (resized.height - target_h) // 2 return resized.crop((left, top, left + target_w, top + target_h)) def _fit_contain(img: Image.Image, size: tuple[int, int]) -> Image.Image: target_w, target_h = size src_w, src_h = img.size scale = min(target_w / src_w, target_h / src_h) resized = img.resize( (round(src_w * scale), round(src_h * scale)), Image.Resampling.LANCZOS, ) canvas = Image.new("RGBA", size, (255, 255, 255, 255)) x = (target_w - resized.width) // 2 y = (target_h - resized.height) // 2 canvas.alpha_composite(resized, (x, y)) return canvas def _parse_size(value: str) -> tuple[int, int]: try: w_text, h_text = value.lower().split("x", 1) width = int(w_text.strip()) height = int(h_text.strip()) except Exception as exc: raise argparse.ArgumentTypeError( "Size must be formatted like 1200x628" ) from exc if width < 300 or height < 300: raise argparse.ArgumentTypeError("Size must be at least 300x300") return width, height def finalize_image( source: Path, logo: Path, output: Path, size: tuple[int, int], fit: str, logo_width_pct: float, margin_pct: float, plate: bool, ) -> None: base = _open_rgba(source) canvas = _fit_cover(base, size) if fit == "cover" else _fit_contain(base, size) logo_img = _open_rgba(logo) max_logo_w = round(size[0] * logo_width_pct / 100) max_logo_h = round(size[1] * 0.12) logo_img.thumbnail((max_logo_w, max_logo_h), Image.Resampling.LANCZOS) margin = round(size[0] * margin_pct / 100) plate_pad_x = max(12, round(logo_img.width * 0.24)) plate_pad_y = max(8, round(logo_img.height * 0.22)) logo_x = size[0] - margin - logo_img.width logo_y = size[1] - margin - logo_img.height if plate: draw = ImageDraw.Draw(canvas) rect = ( logo_x - plate_pad_x, logo_y - plate_pad_y, logo_x + logo_img.width + plate_pad_x, logo_y + logo_img.height + plate_pad_y, ) radius = max(10, round(min(logo_img.size) * 0.22)) draw.rounded_rectangle( rect, radius=radius, fill=(255, 255, 255, 225), outline=(229, 231, 235, 160), width=1, ) canvas.alpha_composite(logo_img, (logo_x, logo_y)) output.parent.mkdir(parents=True, exist_ok=True) rgb = ImageOps.exif_transpose(canvas).convert("RGB") rgb.save(output, quality=95, optimize=True) def main() -> None: parser = argparse.ArgumentParser( description="Resize a generated image and overlay the company logo.", ) parser.add_argument("--source", required=True, type=Path) parser.add_argument("--logo", required=True, type=Path) parser.add_argument("--out", required=True, type=Path) parser.add_argument("--size", type=_parse_size, default=(1200, 628)) parser.add_argument("--fit", choices=("cover", "contain"), default="cover") parser.add_argument("--logo-width-pct", type=float, default=12.0) parser.add_argument("--margin-pct", type=float, default=3.0) parser.add_argument( "--no-plate", action="store_true", help="Do not draw a subtle white plate behind the logo.", ) args = parser.parse_args() finalize_image( source=args.source, logo=args.logo, output=args.out, size=args.size, fit=args.fit, logo_width_pct=args.logo_width_pct, margin_pct=args.margin_pct, plate=not args.no_plate, ) print(f"Saved branded image: {args.out}") if __name__ == "__main__": main()