Creative Blueprint
BlueprintWorked exampleBefore / AfterSam batch — finalAll adsSkillsLessonsProtocols

← Both skills · static-ads · scripts/adformat.py

static-ads · files
static-ads/scripts/adformat.py · 88 lines
Sets every ad to its format: 1080 wide, square 1080×1080, 4:5 1080×1350, Story 1080×1920 (rule D15).
Download the whole skill (zip)

adformat.py

#!/usr/bin/env python3
"""D15 — output format of every ad (Adrien, Oct08 Loom): width is ALWAYS 1080.
  square template (1:1)      -> 1080 x 1080
  Story / TikTok (9:16)      -> 1080 x 1920 (stays a Story)
  4:5 feed                   -> 1080 x 1350
  any other ratio            -> 1080 x round(1080 * h / w)  (the template's own ratio)
A template within 3 % of a standard ratio snaps to it (1856x2304 = 0.806 -> 4:5; 1122x1402 -> 4:5).
usage:  python3 adformat.py check FILE...            -> prints size + target, exit 1 if any file is off
        python3 adformat.py fix FILE [TEMPLATE]      -> crop (least-content window) + resize in place
        python3 adformat.py target W H               -> prints the target size"""
import sys
W_OUT = 1080
STD = [(1, 1), (4, 5), (9, 16)]


def target_size(w, h):
    """(w, h) of a template or image -> the (1080, H) output size."""
    r = w / h
    for a, b in STD:
        if abs(r - a / b) / (a / b) <= 0.03:
            return W_OUT, round(W_OUT * b / a)
    return W_OUT, round(W_OUT * h / w)


def ratio_name(w, h):
    r = w / h
    for a, b in STD:
        if abs(r - a / b) / (a / b) <= 0.03:
            return f"{a}:{b}"
    return f"{w}:{h}"


def _smart_crop(im, tgt):
    """crop to ratio tgt (w/h) keeping the window that cuts the LEAST strong edges (text, objects)."""
    import numpy as np
    from PIL import ImageFilter, Image
    w, h = im.size
    if abs(w / h - tgt) <= 0.002:
        return im
    g = im.convert("L").resize((max(1, w // 4), max(1, h // 4)))
    e = (np.asarray(g.filter(ImageFilter.FIND_EDGES), dtype=float) > 70).astype(float)
    e[:2, :] = 0; e[-2:, :] = 0; e[:, :2] = 0; e[:, -2:] = 0
    e = np.asarray(Image.fromarray((e * 255).astype("uint8")).resize((w, h)), dtype=float)
    if w / h > tgt:
        nw = round(h * tgt); col = e.mean(axis=0); cs = np.concatenate([[0], np.cumsum(col)])
        best = min(range(0, w - nw + 1, 2), key=lambda x: cs[x] + (cs[-1] - cs[x + nw]))
        return im.crop((best, 0, best + nw, h))
    nh = round(w / tgt); row = e.mean(axis=1); cs = np.concatenate([[0], np.cumsum(row)])
    best = min(range(0, h - nh + 1, 2), key=lambda y: cs[y] + (cs[-1] - cs[y + nh]))
    return im.crop((0, best, w, best + nh))


def fit(im, size):
    """PIL image -> exactly `size` (crop to its ratio first, never stretch)."""
    from PIL import Image
    tw, th = size
    if im.size == (tw, th):
        return im
    return _smart_crop(im, tw / th).resize((tw, th), Image.LANCZOS)


def normalize(path, template=None):
    """Rewrite `path` in place at the D15 size of `template` (or of the image itself). Returns (old, new) sizes."""
    from PIL import Image
    im = Image.open(path); old = im.size
    ref = Image.open(template).size if template else old
    size = target_size(*ref)
    if old != size:
        mode = "RGBA" if im.mode in ("RGBA", "LA", "P") and path.lower().endswith(".png") else "RGB"
        fit(im.convert(mode), size).save(path)
    return old, size


if __name__ == "__main__":
    from PIL import Image
    cmd = sys.argv[1] if len(sys.argv) > 1 else ""
    if cmd == "target":
        print(*target_size(int(sys.argv[2]), int(sys.argv[3])))
    elif cmd == "check":
        bad = 0
        for f in sys.argv[2:]:
            s = Image.open(f).size; t = target_size(*s); ok = s == t; bad += not ok
            print(f"{'OK ' if ok else 'BAD'} {s[0]}x{s[1]} target {t[0]}x{t[1]} ({ratio_name(*s)})  {f}")
        sys.exit(1 if bad else 0)
    elif cmd == "fix":
        print(normalize(sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None))
    else:
        print(__doc__); sys.exit(2)