Skip to content

fix(media): keep transparency in image thumbnails - #8772

Closed
johnmatthewtennant wants to merge 2 commits into
mainfrom
jtennant/simone/alpha-thumbnails
Closed

johnmatthewtennant wants to merge 2 commits into
mainfrom
jtennant/simone/alpha-thumbnails

Conversation

@johnmatthewtennant

@johnmatthewtennant johnmatthewtennant commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes a bug: image thumbnails lose transparency. The relay encodes every thumbnail as JPEG, so the transparent pixels of a PNG, WebP or GIF upload turn black. Clients use {sha256}.thumb.jpg for small views, so transparent agent avatars appear on a black square.

Now, when a thumbnail has transparent pixels, the relay writes it as PNG and serves it with Content-Type: image/png. Opaque images keep JPEG thumbnails.

  • The public thumbnail path stays {sha256}.thumb.jpg. Clients build that path, and some only accept that suffix, so none of them needs a change.
  • The PNG is stored under its own key, {sha256}.thumb.png, and the sidecar records thumb_mime: "image/png". GET and HEAD of .thumb.jpg serve whichever object the sidecar names, with its Content-Type. Sidecars without the field still mean JPEG.
  • The imeta thumb check looks for the recorded thumbnail object, so messages with a transparent image's thumbnail are still accepted.
  • The bucket index classifies .thumb.png keys as thumbnails and adds the bytes of both thumbnail keys when a sha has both.
  • Only images with an alpha channel are scanned for transparent pixels.

Images uploaded before this change keep their JPEG thumbnails. Re-uploading the same bytes reuses the existing sidecar, so a backfill would be needed to fix those.

Related issue

Fixes AIDA-2308 (internal tracker: transparent agent avatars render on black). No matching public issue or PR found.

Testing

Red/green against a local relay with Postgres, Redis and MinIO:

cargo test -p buzz-test-client --test e2e_media --test e2e_media_extended -- --ignored

Before (relay at main), the new transparent-PNG test fails because the thumbnail is still a JPEG:

thread 'test_transparent_png_thumbnail_keeps_alpha' panicked at crates/buzz-test-client/tests/e2e_media.rs:272:5:
assertion `left == right` failed
  left: "image/jpeg"
 right: "image/png"

After, the media e2e tests pass, including:

  • test_transparent_png_thumbnail_keeps_alpha: uploads a half-transparent PNG. HEAD and GET of {sha256}.thumb.jpg return image/png, transparent pixels decode with alpha 0, and opaque pixels keep their color.
  • test_upload_and_get: an opaque JPEG's thumbnail is still image/jpeg.
  • test_ws_imeta_thumb_of_transparent_png: a kind 9 with an imeta thumb for the transparent upload is accepted. Without the imeta change, the relay rejects it with imeta thumb references missing thumbnail.

johnmatthewtennant and others added 2 commits October 30, 2026 14:51
The relay encoded every image thumbnail as JPEG, so transparent pixels
in PNG, WebP and GIF uploads turned black. Clients use the thumbnail
for small views, most visibly avatars.

When a thumbnail has transparent pixels, write it as PNG under
`{sha256}.thumb.png` and record `thumb_mime` in the sidecar. The public
path stays `{sha256}.thumb.jpg`: GET and HEAD serve whichever object the
sidecar records, with its Content-Type, so existing clients need no
change. The imeta thumbnail check and the bucket index follow the
recorded key. Opaque images keep JPEG thumbnails, and only images with
an alpha channel are scanned.

Images uploaded before this keep their JPEG thumbnails.

Fixes AIDA-2308

Co-authored-by: Simone <0828be588d5c9c4d092c5b1cf01761d5aef63d6b104807fd8e8284d517dd1a99@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Matthew Tennant <[email protected]>
A transparent GIF avatar showed the same black background as a PNG.
Run the transparent thumbnail test for GIF as well as PNG.

Refs AIDA-2308

Co-authored-by: Simone <0828be588d5c9c4d092c5b1cf01761d5aef63d6b104807fd8e8284d517dd1a99@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Matthew Tennant <[email protected]>
johnmatthewtennant added a commit that referenced this pull request Nov 4, 2026
## Summary

This fixes transparent images showing a black background. Image
thumbnails were always JPEG, which can't store transparency, so a
transparent PNG or GIF upload, such as an agent avatar, rendered on
black wherever a client showed its thumbnail.

The relay now writes a lossy WebP thumbnail at `{sha256}.thumb.webp`,
which keeps transparency, alongside the existing `{sha256}.thumb.jpg`:

- **Encoding:** the WebP comes from libwebp through the `webp` crate,
built from vendored C source, so the relay needs no system library. A
GIF uses its first frame.
- **Upload responses:** the upload descriptor's `thumb`, and its
per-tenant rewrite, point to the WebP.
- **JPEG kept:** installed clients derive `.thumb.jpg` themselves, so
the relay keeps writing it until clients move to WebP with a JPEG
fallback. Existing images still have only their JPEG thumbnail.
- **Serving:** `/media/` serves both variants, each with its own content
type. Other `.thumb.*` extensions still return 404.
- **imeta:** a `thumb` may name either variant, and the relay checks
that the named object exists.
- **Storage sweep:** the bucket taxonomy counts both variants as the
image's thumbnail bytes instead of classifying `.thumb.webp` as unknown.

| 320px thumbnail | JPEG | PNG | lossy WebP (q80) |
|---|---|---|---|
| Transparent avatar (starter-team `fizz.png`) | 11.3 KB, black
background | 65.1 KB | 13.8 KB |
| Opaque avatar (`sprout-agent.png`) | 23.6 KB | 156.5 KB | 12.3 KB |
| Screenshot (`channel-thread.png`) | 13.4 KB | 62.5 KB | 6.4 KB |

Encoding a WebP thumbnail takes 3 to 20 ms.

### Related issue

Supersedes #8772, which served a PNG at the `.thumb.jpg` path.

### Testing

Red/green against a local relay on `:3000`, built from the branch under
test, with Postgres, Redis and MinIO from `docker compose`:

```bash
cargo test -p buzz-test-client --test e2e_media test_transparent_png_thumbnail_is_webp_with_alpha -- --ignored
cargo test -p buzz-test-client --test e2e_media_extended test_ws_imeta_webp_thumb -- --ignored
```

- **On `main`:** both tests fail. The descriptor's `thumb` is
`.thumb.jpg`, and the WebSocket send is rejected with `invalid: imeta
thumb must be a local .thumb.jpg path`.
- **On this branch:** both tests pass:
- Uploading a half-transparent PNG returns a `thumb` ending
`.thumb.webp`.
  - HEAD and GET on that URL return `Content-Type: image/webp`.
- The decoded thumbnail keeps alpha 0 on the transparent half and alpha
255 on the opaque half.
  - `.thumb.jpg` still returns `image/jpeg`.
- A kind 9 message whose imeta `thumb` names the `.thumb.webp` is
accepted.

## Screenshots

The starter-team `fizz.png` avatar uploaded to a relay built from this
branch. Both thumbnails are shown as round avatars beside the original
PNG. `.thumb.jpg` fills the transparent area with a solid disc, and
`.thumb.webp` keeps the transparency.

![Transparent avatar thumbnails: .thumb.jpg, .thumb.webp and the
original PNG on light and dark
backgrounds](https://raw.githubusercontent.com/block/buzz/9ee1c633d8158ba401efcf738eb5676070ebd9cf/pr-9003--transparent-avatar-thumbnails.png)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: John Matthew Tennant <[email protected]>
Co-authored-by: Simone <0828be588d5c9c4d092c5b1cf01761d5aef63d6b104807fd8e8284d517dd1a99@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
@johnmatthewtennant

Copy link
Copy Markdown
Contributor Author

Superseded by #9003 (WebP thumbnails), merged 2026-11-04.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant