Bluesky avatar and banner: 1,000,000 bytes, PNG or JPEG
Bluesky's profile record caps avatar and banner at 1,000,000 bytes each, PNG or JPEG only. How to size a GPT Image 2.5 output and check the bytes in Pillow.
Bluesky's profile record accepts PNG or JPEG for both the avatar and the banner, and each blob is capped at 1,000,000 bytes. That is a decimal million, so a file of exactly 1 MiB (1,048,576 bytes) is over the line. The lexicon sets no pixel size, so pick dimensions for how the picture looks and then check the byte count.
The limits, from the lexicon
The limits come straight from Bluesky's own lexicon file for app.bsky.actor.profile, read today. Nothing in that file mentions pixel dimensions or crop rules, so this post does not guess any.
| Field | Type | Accepted | Limit |
|---|---|---|---|
| displayName | string | text | 64 graphemes (640 bytes) |
| description | string | text | 256 graphemes (2,560 bytes) |
| avatar | blob | image/png, image/jpeg | maxSize 1000000 |
| banner | blob | image/png, image/jpeg | maxSize 1000000 |
Which sizes Sume can produce
Sume's image API lists ChatGPT Image 2.5 as openai/gpt-image-2.5. Its custom pixel sizes need both edges to be multiples of 16, a longest edge of at most 3840, an aspect ratio of at most 3:1, and 655,360 to 8,294,400 pixels in total.
For an avatar, 1024x1024 qualifies (1,048,576 pixels). For a wide banner, 1536x512 is exactly 3:1 and has 786,432 pixels, so it also qualifies. Ask for output_format jpeg, since the docs list png, jpeg, webp and svg as formats.
curl -X POST https://api.sume.com/v1/images \
-H "Authorization: Bearer $SUME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-image-2.5",
"prompt": "wide abstract banner, soft teal gradient, no text",
"image_size": "1536x512",
"output_format": "jpeg"
}'You cannot ask for a smaller file
The docs list output_compression (0 to 100 for webp and jpeg) in the schema but say no model advertises it yet, and sending it returns 400 unsupported_parameter. So you cannot ask Sume for a smaller file at request time. Size control happens after download.
- Request the pixels you need, as jpeg.
- If the response is 200, the image is in data[0].url. If it is 202, poll the job envelope as described in the docs and fetch the result.
- Download the file and measure it before you upload anywhere.
Fit it under the cap
This loop re-saves a local file as JPEG at falling quality until it is at most 1,000,000 bytes. If it never gets there, the picture has too many pixels or too much fine detail for the cap, and you should shrink it.
import asyncio, io
from PIL import Image
LIMIT = 1_000_000 # decimal bytes, not 1 MiB
async def main():
img = Image.open("avatar.png").convert("RGB")
for quality in range(95, 40, -5):
buf = io.BytesIO()
img.save(buf, "JPEG", quality=quality, optimize=True)
if buf.tell() <= LIMIT:
with open("avatar.jpg", "wb") as f:
f.write(buf.getvalue())
print("quality", quality, "bytes", buf.tell())
return
raise SystemExit("still over 1,000,000 bytes: shrink the pixels")
asyncio.run(main())Mistakes to avoid
- Comparing against 1,048,576. The lexicon number is 1000000.
- Uploading the PNG you just generated without measuring. Smooth gradients and noise both change PNG size a lot.
- Sending output_compression to Sume. It returns 400 in v1.
- Throwing away the original. Keep the generated file and the job record in case a platform asks where an image came from.
Sources
Related posts
More in Developers
- Bluesky link card thumbnail: 1,000,000 bytes, any image type
The thumb on a Bluesky link card is optional, accepts image/* and is capped at 1,000,000 bytes. Make a 1280x720 preview with Sume and shrink it under the cap.
- Browser voice app that starts Sume jobs: keep the key on your server
Voice apps run in the browser over WebRTC, but Sume keys belong on a server. A route handler that holds the key, allowlists models, reuses idempotency keys.
- Sume bulk queue 404 format_run_queue_not_found: three causes
A Sume bulk queue poll returned 404 format_run_queue_not_found. The id is wrong or the queue is another owner's. How to tell which, and what to do next.
- Bulk run 409 idempotency_conflict: find the first queue by queue_id
A Sume bulk create that reuses a key with a different payload returns 409 with details.queue_id. How to read the original queue and decide what to resend.
Written by Sume