A4 poster at 300 dpi: 2480x3508 is over the GPT Image 2.5 pixel cap

A4 at 300 dpi is 2480x3508, 8,699,840 pixels, above the 8,294,400 cap. Render 2400x3392 on GPT Image 2.5, then scale up 3.4% in Pillow and tag it 300 dpi.

5 min readSume
All posts

An A4 poster at 300 dpi is 2480x3508 pixels, and that is 8,699,840 pixels, which is over the 8,294,400-pixel ceiling the OpenAI guide gives for GPT Image 2.5 custom sizes. The fix is to render 2400x3392 (8,140,800 pixels, same shape within 0.1 percent) and scale it up by about 3.4 percent in Pillow.

The arithmetic is simple. A4 is 210 x 297 mm, or 8.268 x 11.693 inches. At 300 dpi that is 2480 x 3508 pixels. A 3.4 percent enlargement from a clean render is a small step compared with printing a 1024-pixel image at A4.

Why 2400x3392

Both edges must be multiples of 16, so 2480 and 3508 are out (3508 / 16 = 219.25). 2400x3392 is a legal box close to the A4 ratio of 1.4142: both edges are multiples of 16, it stays under the pixel cap, and its ratio of 1.4133 is within a tenth of a percent.

A4 at 300 dpi against the GPT Image 2.5 custom-size rules (read 2026-10-05)
Check2480x3508 direct2400x3392 render
Both edges multiples of 16no (3508 / 16 = 219.25)yes (150 x 16, 212 x 16)
Longest edge at most 38403508, pass3392, pass
Aspect ratio at most 3:11.414:1, pass1.413:1, pass
Pixels 655,360 to 8,294,4008,699,840, fail8,140,800, pass

Code

The fit call enlarges the render to cover 2480x3508 and trims a fraction of a pixel row. The saved PNG carries a 300 dpi tag so print software reads the intended size.

import os, requests
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def generate(body):
    r = requests.post("https://api.sume.com/v1/images", headers=H, json=body, timeout=60)
    if r.status_code != 200:  # 202 = still running, read data.status_url
        raise SystemExit(f"{r.status_code}: {r.text[:300]}")
    return r.json()["data"][0]["url"]
from io import BytesIO
from PIL import Image, ImageOps

url = generate({
    "model": "openai/gpt-image-2.5",
    "prompt": "Minimalist concert poster, a single lit stage microphone in an empty hall, large calm area at the top for a title",
    "image_size": {"width": 2400, "height": 3392},
    "quality": "high",
})
img = Image.open(BytesIO(requests.get(url, timeout=60).content)).convert("RGB")
img = ImageOps.fit(img, (2480, 3508), Image.LANCZOS)
img.save("poster-a4.png", dpi=(300, 300))

Price of a print-size render

A 2400x3392 image has about eight times the pixels of a 1024 square, but at high quality its output-token price is only about 2.4 times the square's (about $0.124 against $0.053 at list). Using the token formula behind the fal Flare price page, the output-only list and Sume prices are below. Sume bills list times 1.25, before the input-token charge for the prompt.

Draft at low, which costs a small fraction of high, and reserve the print-size high call for the approved layout.

Output-only price per 2400x3392 image (read 2026-10-05)
QualityProvider listSume at list x 1.25
low$0.0134$0.0168
medium$0.0311$0.0388
high$0.1241$0.1552
xhigh$0.2191$0.2738

Set the text in code

At print size, misspelled or warped lettering is expensive to discover. Ask the model for the artwork with a clean empty area, then set the title, date and venue in your layout tool or in Pillow. The OpenAI guide itself notes that text rendering and composition control remain challenging areas, so the safer production path is to keep exact copy out of the generated pixels.

If the call returns 202 instead of 200

POST /v1/images waits up to 30 seconds and answers 200 with the images when the job finishes in time. When it does not, the route answers 202 with the standard job envelope, and the images come from GET /v1/jobs/{id}/result. Slow settings such as 4K, high quality and large n are the likeliest to fall back. A 1-2 megapixel ad master at high quality usually stays in the wait budget, but the code should branch on the status code, not the body shape.

For a batch of ad sizes, send each request with mode: "async" and read the results afterwards, or add a webhook_url with mode: "webhook". A failed synchronous job returns 502 with an error code and a next_action, and Sume does not bill failed generations.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume