Editing an image five times with AI: keep PNG between passes
Each JPEG save adds error that stays in every later edit. A Pillow test of PNG against JPEG over five passes, and the format to request from Sume.

In a multi-step edit, save every intermediate result as PNG and convert to JPEG once, at the end. PNG is lossless, so five passes through it leave pixels exactly as the model returned them. JPEG throws information away each time it is written, and the loss from the first save stays in every later pass.
FLUX 3 Image's pitch is multi-step edits that leave the rest of the picture alone, and BFL's docs describe elements that persist across turns. Whatever model you edit with, a lossy format between steps works against that goal. On Sume, the Image API lists png, jpeg and webp as output_format on models that advertise formats, so you can ask for PNG at each step.
A five-pass test
This script builds a 512 x 512 synthetic test image (a gradient, a hard-edged block and a little noise), saves and reloads it five times in each format, and prints the mean absolute error against the original on the 0 to 255 scale. It is not model output, so treat the numbers as an illustration of the mechanism rather than a measure of any generator.
import io
import numpy as np
from PIL import Image
rng = np.random.default_rng(7)
x = np.linspace(0, 255, 512)
base = np.stack([np.tile(x, (512, 1)), np.tile(x[:, None], (1, 512)), np.full((512, 512), 120.0)], axis=2)
base[128:384, 128:384] = (30, 200, 90) # a hard-edged block
img = np.clip(base + rng.normal(0, 3, base.shape), 0, 255)
start = Image.fromarray(img.astype("uint8"))
def reencode(im, fmt, **kw):
buf = io.BytesIO()
im.save(buf, fmt, **kw)
return Image.open(io.BytesIO(buf.getvalue())).convert("RGB")
for label, fmt, kw in [("png", "PNG", {}), ("jpeg q90", "JPEG", {"quality": 90}), ("jpeg q75", "JPEG", {"quality": 75})]:
cur = start
row = []
for edit in range(1, 6):
cur = reencode(cur, fmt, **kw)
err = np.abs(np.asarray(cur, dtype=float) - np.asarray(start, dtype=float)).mean()
row.append(f"{err:.2f}")
print(f"{label:9}", " ".join(row))What it printed
On my run the PNG row was 0.00 at every pass. The JPEG rows started at about 2.5 after the first save and crept up to 2.8 by the fifth at quality 90, and from 2.6 to 2.9 at quality 75. Most of the damage is the first write; later passes add a little each. A drift of under three levels is invisible on its own, which is exactly why it gets ignored, and why a second tool that compares the result to a reference will then flag it.
| Format | Pass 1 error | Pass 5 error | Verdict for intermediates |
|---|---|---|---|
| PNG | 0.00 | 0.00 | Safe |
| JPEG quality 90 | 2.50 | 2.80 | Avoid |
| JPEG quality 75 | 2.60 | 2.94 | Avoid |
What to request from Sume
Ask for output_format: "png" on each intermediate. The catch is that not every model accepts the field: per the docs, a request that sets a parameter the model does not list is rejected with 400 unsupported_parameter, and a few catalog rows list no output formats at all (Ideogram 4.5 and Higgsfield Soul pick the format themselves). Read supported_parameters.output_format from GET /v1/images/models first.
Recraft V4 is the other exception: its catalog row lists WebP only, which is lossy by default for many encoders, so convert it once to PNG before any further edit and treat that conversion as the new first generation.
Where JPEG is fine
If you store intermediates for audit, PNG files are large, so keep them only for the length of the edit session and delete them once the final export is approved. Sume's docs describe result URLs as Sume-hosted and signed, so download each result as soon as it arrives rather than saving only the link.
Convert at the end, once, for delivery: a JPEG at quality 85 to 92 is the right final format for most photographic stills on the web. And if each step is a masked edit, you can side-step accumulating loss entirely by compositing the original back outside the mask, as in edit one region and composite the rest back.
Related field behavior is in the Image API docs, and GPT Image 2.5 edit drift covers the separate problem of the model itself changing things across turns.
Sources
Related posts
More in Developers
- AI presenter video from a script: first clip in Python on Sume
Make an AI presenter clip from a script in one Python file: submit to /v1/avatar-1.0/talking-video, poll to terminal, read the result. Costs and limits.
- AI SDK MCP tools(): explicit Zod schemas for Sume's jobs_wait
Pass explicit Zod schemas to mcpClient.tools() so your app exposes only the Sume tools it needs, with typed job ids and a bounded wait_for.
- AI video defaults on Sume: 768p, 480p, 720p and 8 seconds explained
Recast defaults to 768p, Genjutsu to 480p, Gemini Omni Flash and sume/auto to 720p and 8 s. Which defaults change price, and the one place fal differs.
- AI video models with no aspect_ratio option: Recast, Genjutsu, Grok
Three Sume video rows reject aspect_ratio: H3 Max Recast, Higgsfield Genjutsu and Grok Imagine Video 1.5. What sets the output frame instead, and what to send.
Written by Sume