Files

151 lines
4.6 KiB
Python
Raw Permalink Normal View History

2026-08-01 10:28:22 +05:30
#!/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()