Sale badge on a product image: draw it in Pillow, not in the prompt
Generate a clean product photo on Sume, then draw the sale badge and percentage from your catalog data in Pillow, so a price change never costs a regeneration.

Put a sale badge on a product image by generating the photo without any badge and drawing the badge yourself in Pillow from your catalog data. A model can draw a red circle with 30% OFF, but a wrong digit costs more than the badge is worth, and a price change would need a new billed image. Code gives you the exact number and a free re-render.
For the photo itself, GPT Image 2.5 on Sume is a good fit; fal lists it as OpenAI's default image model, with natural lighting, rich textures and complex layouts (read 2026-10-06, fal). Ask for a clear corner in the prompt so the badge does not cover the product. The Image API docs cover quality, aspect_ratio and image_size.
Split the job
The photo is generated once per product and reused. The badge is a function of two values: the percentage and the position. The same photo can then serve a 20 percent week and a 40 percent week at no extra generation cost.
| Approach | Cost of a new percentage | Risk of a wrong digit |
|---|---|---|
| Badge drawn by the model | One billed generation per change | Needs a proofread each time |
| Badge drawn in Pillow | No generation | Number comes from your data |
Generate, then stamp
The script creates a product photo with an empty top-right corner, downloads it, and draws a circle with the percentage. Move the circle by changing the coordinates; for production use a font file with a bold weight.
import io
import os
import requests
from PIL import Image, ImageDraw
r = requests.post(
"https://api.sume.com/v1/images",
headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
json={
"model": "openai/gpt-image-2.5",
"prompt": "Studio photo of a ceramic coffee mug on a pale grey "
"surface, product centered, empty space in the top-right corner. "
"No text, no badges.",
"aspect_ratio": "1:1",
"quality": "medium",
},
timeout=60,
)
r.raise_for_status()
img = Image.open(io.BytesIO(requests.get(r.json()["data"][0]["url"], timeout=60).content)).convert("RGB")
w, h = img.size
d = ImageDraw.Draw(img)
box = (int(w * 0.70), int(h * 0.05), int(w * 0.95), int(h * 0.05 + w * 0.25))
d.ellipse(box, fill=(200, 30, 30))
d.text((box[0] + 40, box[1] + 55), "-30%", fill="white")
img.save("mug-sale.png")Why the badge belongs to your code
A badge is a statement of fact: the product is on sale, at this percentage, until this date. Those facts live in your catalog and change on your schedule, not in a model. Drawing the badge from the catalog means a price change is a re-render of a cheap local step instead of a billed generation, and it means no one has to proofread 400 images for a mistyped percentage. Some marketplaces also set rules on text and overlays in main images, so check the channel's rules; a badge may be fine on your own store and not on a marketplace listing.
Batch it
For a catalog, loop over your products: generate each photo once, save it, and keep the file path in your database. A second script reads the sale table and stamps badges onto copies of those files. Because the stamping step makes no API calls, you can run it on every price change, for any number of products, and the only billed work is the first generation per product.
Keep the original, unstamped photo. If you overwrite it with the badged version, you cannot remove the badge when the sale ends.
- Store the clean photo and the stamped copy as separate files.
- Name stamped files with the percentage, such as mug-sale-30.png.
- Delete or replace stamped copies when the sale window closes.
- Log which percentage each live image shows, so audits are quick.
Font and size
The default Pillow font is small. Load a bold TrueType file with ImageFont.truetype and size it relative to the image width, for example a tenth of the width, so the badge looks the same on a 1024 pixel image and a 2048 pixel one. Center the text by measuring it with the draw object's text bounding box rather than guessing offsets.
Keep the badge honest
The badge must match the real price. Many regions regulate sale and reference-price claims, so have the percentage computed from the same price table that your checkout uses, and render the image from the same job that publishes the price. If the sale ends, regenerate the badge image or remove it, and make sure the old file is no longer served from a cache.
If the product has a label that must be legible, composite the real label rather than trusting a model with it; see a supplement bottle ad with a legible label.
Sources
Related posts
More in Use cases
- Search what was said across clips: SQLite FTS5 from STT segments
Request sentence segmentation on Sume STT, load each segment into a SQLite FTS5 table, and find the clip and second where a phrase was spoken.
- How many characters is a 15, 30, 45 or 60 second AI narration
Using 750 characters per minute of speech, a 15-second voiceover is 188 characters, 30 seconds is 375, 60 seconds is 750. Sume TTS cost for each.
- Minimum video length to earn on Shorts, Reels and TikTok in 2026
The length and view thresholds creators cite for YouTube Shorts, Reels and TikTok payouts, with sources and caveats, plus how to render to a length target.
- Shorts season trailer: first 5 seconds of every episode in one clip
Build a Shorts season trailer by trimming 0 to 5 s from each episode with video trim, then planning one timeline render under a music bed. Python included.
Written by Sume