Independent FastAPI backend for the MaskanX agentic growth platform. Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn, HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage for chats and cron jobs, provider and secret management, and the CLI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
4.6 KiB
Python
151 lines
4.6 KiB
Python
#!/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()
|