AI coaster set: four designs in one call, circle-cropped in Pillow

Generate a four-design coaster set with n=4 at 1:1, then crop each to a circle with a Pillow mask. Request, crop code and a bleed note, with the cost to check.

5 min readSume
All posts

A coaster set is four related designs on round or square blanks, which maps cleanly to one image request with n: 4 at a 1:1 ratio, followed by a circle crop in code. On Sume, GPT Image 2.5 accepts n from 1 to 4, so one call returns four candidates, and Pillow turns each square into a circle with a mask.

Ask for designs that work when cropped: a centred motif with generous margins, so the circle does not cut the subject, and a pattern or colour that continues to the edge so there is no thin white ring if the print is slightly off. That second point is called bleed, and it is the reason to generate the full square and crop later.

One call, four designs

Describe a theme and let the model vary the motif: a set of four botanical line drawings in one colour palette, centred, with a tinted background to the edge. The response's usage.cost is the billed total for the call (Sume Image API docs), so you can tell what a set costs before you order more.

If the call takes longer than 30 seconds Sume returns a 202 job envelope instead; handle it by polling the status URL rather than treating the call as failed.

import os, requests

r = requests.post("https://api.sume.com/v1/images", timeout=120, json={
    "model": "openai/gpt-image-2.5",
    "prompt": ("Four different botanical line drawings for a coaster set: fern, ginkgo, olive branch, "
               "monstera. Each centered with wide margins, one deep green ink on a warm cream "
               "background that fills the whole square, no text"),
    "aspect_ratio": "1:1", "n": 4, "quality": "medium",
}, headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"})
r.raise_for_status()
if r.status_code == 202:
    raise SystemExit("queued: poll the job status_url")
for i, item in enumerate(r.json()["data"]):
    print(i, item["url"])
print("billed for the set:", r.json()["usage"]["cost"])

Circle crop with a mask

Round coasters need a circular image. Pillow draws a filled ellipse on a grayscale mask, and the mask becomes the alpha channel. The function below crops each file to the largest circle that fits, keeps a configurable inset so the motif is not clipped, and saves a transparent PNG.

Keep the original square as the bleed version for the printer. Most print shops want the art to extend past the cut line, so send them the square and use the circle only for previews.

from PIL import Image, ImageDraw

def circle_crop(path, out, inset=0.04):
    img = Image.open(path).convert("RGBA")
    s = min(img.size)
    img = img.crop(((img.width - s) // 2, (img.height - s) // 2,
                    (img.width + s) // 2, (img.height + s) // 2))
    pad = int(s * inset)
    mask = Image.new("L", (s * 4, s * 4), 0)
    ImageDraw.Draw(mask).ellipse([pad * 4, pad * 4, s * 4 - pad * 4, s * 4 - pad * 4], fill=255)
    img.putalpha(mask.resize((s, s), Image.LANCZOS))
    img.save(out)
    return img.size

if __name__ == "__main__":
    Image.new("RGB", (1024, 1024), "#f4ecd8").save("coaster.png")
    print(circle_crop("coaster.png", "coaster-round.png"))

Choosing the best of four

Lay the four side by side in their circles and check that the motif is centred and nothing important is clipped at the edge. If one is weaker, regenerate only that subject with a single request rather than another set of four.

For picking from a set of variants see AI thumbnail variants in one call, and for a wrap-around print example see mug wrap art for print on demand.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume